diff --git a/avy-dired-img/screenshot.png b/avy-dired-img/screenshot.png new file mode 100644 index 0000000..040383e Binary files /dev/null and b/avy-dired-img/screenshot.png differ diff --git a/biome-img/columns.png b/biome-img/columns.png new file mode 100644 index 0000000..49f0c50 Binary files /dev/null and b/biome-img/columns.png differ diff --git a/biome-img/multi.png b/biome-img/multi.png new file mode 100644 index 0000000..aa5ad80 Binary files /dev/null and b/biome-img/multi.png differ diff --git a/biome-img/query.png b/biome-img/query.png new file mode 100644 index 0000000..f83e49e Binary files /dev/null and b/biome-img/query.png differ diff --git a/biome-img/report.png b/biome-img/report.png new file mode 100644 index 0000000..5c74004 Binary files /dev/null and b/biome-img/report.png differ diff --git a/biome-img/root.png b/biome-img/root.png new file mode 100644 index 0000000..8b65dd9 Binary files /dev/null and b/biome-img/root.png differ diff --git a/config/index.html b/config/index.html new file mode 100644 index 0000000..3e7762c --- /dev/null +++ b/config/index.html @@ -0,0 +1,10 @@ + + + + https://sqrtminusone.xyz/configs/readme/ + + + + + + diff --git a/configs/console/index.html b/configs/console/index.html new file mode 100644 index 0000000..dfc6756 --- /dev/null +++ b/configs/console/index.html @@ -0,0 +1,1213 @@ + + + + + + Console + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ Console + +

+
+

+ 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

+
export RIPGREP_CONFIG_PATH=$HOME/.config/ripgrep/ripgreprc
+

hledger path

+
export LEDGER_FILE="$HOME/30-39 Life/32 org-mode/ledger/ledger.journal"
+

Checking if running inside termux

+
if command -v termux-setup-storage > /dev/null; then
+    export IS_ANDROID=true
+    [[ -f ~/.android_profile ]] && . ~/.android_profile
+fi
+

Timezone

+
# TZ='Asia/Karachi'; export TZ
+

My paths

+

My script folders

+
if [ -d "$HOME/bin" ] ; then
+    export PATH="$HOME/bin:$PATH"
+    export PATH="$HOME/bin/scripts:$PATH"
+fi
+
+if [ -d "$HOME/.local/bin" ] ; then
+    export PATH="$HOME/.local/bin:$PATH"
+fi
+

Guix settings

+

Enable extra profiles

+
if [ -z "$IS_ANDROID" ]; then
+    GUIX_EXTRA_PROFILES=$HOME/.guix-extra-profiles
+    for i in $GUIX_EXTRA_PROFILES/*; do
+	profile=$i/$(basename "$i")
+	if [ -f "$profile"/etc/profile ]; then
+	    GUIX_PROFILE="$profile"
+	    . "$GUIX_PROFILE"/etc/profile
+	fi
+	if [ -d "$profile"/share/man ]; then
+	    export MANPATH="${MANPATH:-$(manpath)}:$profile/share/man"
+	fi
+	export XDG_DATA_DIRS="$XDG_DATA_DIRS:$profile/share"
+	unset profile
+    done
+fi
+

Set Jupyter config PATH. It defaults to readonly directory somewhere in Guix profile.

+
export JUPYTER_CONFIG_DIR=$HOME/.config/jupyter
+

Set a folder for my packages.

+
export GUIX_PACKAGE_PATH=~/guix-packages
+

Locale settings

+
export GUIX_LOCPATH=$HOME/.guix-extra-profiles/console/console/lib/locale
+

Somehow LibreOffice doesn’t work without the following:

+
export GIO_EXTRA_MODULES=""
+

Other package managers

+

Using other package managers with Guix requires some extra work.

+

Cask

+
if [ -d "$HOME/.cask" ]; then
+    export PATH="/home/pavel/.cask/bin:$PATH"
+fi
+

Make flatpak apps visible to launchers:

+
if [ -d "$HOME/.local/share/flatpak" ]; then
+    export XDG_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
+    export FONTCONFIG_PATH="$HOME/.guix-extra-profiles/desktop-misc/desktop-misc/etc/fonts"
+fi
+

Make nix apps visible to launchers:

+
if [ -d "$HOME/.nix-profile" ]; then
+    export XDG_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:

+
export NPM_CONFIG_USERCONFIG=$HOME/._npmrc
+

The variable is unset in a script in Guix.org.

+

Set PATH & MANPATH

+
NPM_PACKAGES="${HOME}/.npm-packages"
+
+export PATH="$PATH:$NPM_PACKAGES/bin"
+export MANPATH="${MANPATH:-$(manpath)}:$NPM_PACKAGES/share/man"
+

XResources

+ + + + + + + + + + + +
Guix dependency
xrdb
+
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
+    export MANPATH="/usr/local/texlive/2020/texmf-dist/doc/man:$MANPATH"
+    export INFOPATH="/usr/local/texlive/2020/texmf-dist/doc/info:$INFOPATH"
+    export PATH="/usr/local/texlive/2020/bin/x86_64-linux:$PATH"
+fi
+

Cargo (Rust)

+
if [ -d "$HOME/.cargo" ] ; then
+    export PATH="$HOME/.cargo/bin:$PATH"
+fi
+

RVM (Ruby)

+
if [ -d "$HOME/.rvm" ] ; then
+    export PATH="$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
+    export PATH="$HOME/go/bin:$PATH"
+fi
+

ghcup (Haskell)

+
[ -f "/home/pavel/.ghcup/env" ] && source "/home/pavel/.ghcup/env" # ghcup-env
+

Perl

+
if [ -d "$HOME/perl5" ] ; then
+    PATH="/home/pavel/perl5/bin${PATH:+:${PATH}}"
+    PERL5LIB="/home/pavel/perl5/lib/perl5${PERL5LIB:+:${PERL5LIB}}"; export PERL5LIB;
+    PERL_LOCAL_LIB_ROOT="/home/pavel/perl5${PERL_LOCAL_LIB_ROOT:+:${PERL_LOCAL_LIB_ROOT}}"; export PERL_LOCAL_LIB_ROOT;
+    PERL_MB_OPT="--install_base \"/home/pavel/perl5\""; export PERL_MB_OPT;
+    PERL_MM_OPT="INSTALL_BASE=/home/pavel/perl5"; export PERL_MM_OPT;
+fi
+

Bash

+

.bash_profile

+
[[ -f ~/.profile ]] && . ~/.profile
+
+[[ -f ~/.bashrc ]] && . ~/.bashrc
+

.bashrc

+

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.

+
if [[ $- != *i* ]]
+then
+    [[ -n "$SSH_CLIENT" && -f "/etc/bashrc" ]] && source /etc/profile
+    return
+fi
+

If termux-setup-storage is available, then we’re running inside termux. It is necessary to source ~/.profile manually.

+
if command -v termux-setup-storage > /dev/null; then
+    if [[ -z "$IS_ANDROID" ]]; then
+	source ~/.profile
+    fi
+fi
+

Source the system-wide file

+
if [[ -f "/etc/bashrc" ]]; then
+    source /etc/bashrc
+fi
+
+ + + + + + + + + + +
Guix dependency
xhost
+

Allow other users to access X server. Necessary for stuff like aw-watcher-window.

+
xhost +local:root > /dev/null 2>&1
+

Set manpager to bat

+
export MANPAGER="sh -c 'sed -e s/.\\\\x08//g | bat -l man -p'"
+

eat integration

+
[ -n "$EAT_SHELL_INTEGRATION_DIR" ] && source "$EAT_SHELL_INTEGRATION_DIR/bash"
+

Launch fish

+

Launch fish shell unless bash itself is launched from fish.

+
use_fish=true
+
+if [[ $(ps --no-header --pid=$PPID --format=cmd) != "fish" && ${use_fish} && $(command -v fish) ]]
+then
+    exec fish
+fi
+

The rest of .bashrc is not executed if fish was launched.

+

Colors

+

Setting for colors, packed in the default .bashrc in Manjaro

+
use_color=true
+
+# Set colorful PS1 only on colorful terminals.
+# dircolors --print-database uses its own built-in database
+# instead of using /etc/DIR_COLORS.  Try to use the external file
+# first to take advantage of user additions.  Use internal bash
+# globbing instead of external grep binary.
+safe_term=${TERM//[^[:alnum:]]/?}   # sanitize TERM
+match_lhs=""
+[[ -f ~/.dir_colors   ]] && match_lhs="${match_lhs}$(<~/.dir_colors)"
+[[ -f /etc/DIR_COLORS ]] && match_lhs="${match_lhs}$(</etc/DIR_COLORS)"
+[[ -z ${match_lhs}    ]] \
+    && type -P dircolors >/dev/null \
+    && match_lhs=$(dircolors --print-database)
+[[ $'\n'${match_lhs} == *$'\n'"TERM "${safe_term}* ]] && use_color=true
+
+if ${use_color} ; then
+    # Enable colors for ls, etc.  Prefer ~/.dir_colors #64489
+    if type -P dircolors >/dev/null ; then
+	if [[ -f ~/.dir_colors ]] ; then
+	    eval $(dircolors -b ~/.dir_colors)
+	elif [[ -f /etc/DIR_COLORS ]] ; then
+	    eval $(dircolors -b /etc/DIR_COLORS)
+	fi
+    fi
+
+    if [[ ${EUID} == 0 ]] ; then
+	PS1='\[\033[01;31m\][\h\[\033[01;36m\] \W\[\033[01;31m\]]\$\[\033[00m\] '
+    else
+	PS1='\[\033[01;32m\][\u@\h\[\033[01;37m\] \W\[\033[01;32m\]]\$\[\033[00m\] '
+    fi
+
+    alias ls='ls --color=auto'
+    alias grep='grep --colour=auto'
+    alias egrep='egrep --colour=auto'
+    alias fgrep='fgrep --colour=auto'
+else
+    if [[ ${EUID} == 0 ]] ; then
+	# show root@ when we don't have colors
+	PS1='\u@\h \W \$ '
+    else
+	PS1='\u@\h \w \$ '
+    fi
+fi
+
+unset use_color safe_term match_lhs sh
+

Settings

+

Some general bash settings.

+

References:

+ + +
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)
+

History control

+
shopt -s histappend
+export HISTCONTROL=ignoredups:erasedups
+HISTSIZE=
+HISTFILESIZE=
+

Autocompletions

+
[ -r /usr/share/bash-completion/bash_completion ] && . /usr/share/bash-completion/bash_completion
+if [ -d "/usr/share/fzf" ]; then
+    source /usr/share/fzf/completion.bash
+    source /usr/share/fzf/key-bindings.bash
+fi
+

Aliases

+
alias v="vim"
+if command -v exa > /dev/null; then
+    alias ls="exa --icons"
+    alias ll="exa -lah --icons"
+else
+    alias ll='ls -lah'
+fi
+alias q="exit"
+alias c="clear"
+alias ci="init_mamba"
+alias ca="micromamba activate"
+alias cii="export INIT_MAMBA=true && init_mamba"
+
if [[ ! -z "$SIMPLE" ]]; then
+    unalias ls
+    alias ll="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 () {
+    export MAMBA_EXE="/home/pavel/.guix-extra-profiles/dev/dev/bin/micromamba";
+    export MAMBA_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
+	    export  PATH="/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' ]; then source '/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' ]; then source '/home/pavel/yandex-cloud/completion.bash.inc'; fi
+}
+

Fish

+ + + + + + + + + + + + + +
Guix dependencyDescription
fishAn alternative non POSIX-compliant shell
+

Fish shell is a non-POSIX-compliant shell, which offers some fancy UI & UX features.

+

Launch starship

+
if [ "$TERM" != "dumb" ]; and type -q starship;
+    starship init fish | source
+else
+    function fish_prompt -d "Write out the prompt"
+	printf '%s@%s %s%s%s > ' $USER $hostname \
+	    (set_color $fish_color_cwd) (basename (pwd)) (set_color normal)
+    end
+end
+

Enable vi keybindings & aliases. The alias syntax is the same as in bash, so it’s just a noweb reference to .bashrc.

+
fish_vi_key_bindings
+
+alias q="exit"
+alias c="clear"
+if type -q exa
+    alias ls="exa --icons"
+    alias ll="exa -lah --icons"
+else
+    alias ll="ls -h"
+end
+
+ + + + + + + + + + +
Guix dependency
dt-colorscripts
+

Launch a random DT’s colorscript unless ran inside tmux or Emacs.

+
if ! test -n "$TMUX"; and ! test -n "$IS_EMACS"; and type -q colorscript
+    colorscript random
+end
+

Suppress fish greeting

+
set fish_greeting
+

Micromamba

+

First, a function to initialize micromamba.

+
function init_mamba
+    set -gx MAMBA_EXE "/home/pavel/.guix-extra-profiles/dev/dev/bin/micromamba"
+    set -gx MAMBA_ROOT_PREFIX "/home/pavel/micromamba"
+    $MAMBA_EXE shell hook --shell fish --prefix $MAMBA_ROOT_PREFIX | source
+end
+
+if test -n "$INIT_MAMBA";
+    init_mamba
+end
+
+alias ca="micromamba activate"
+alias ci="init_mamba"
+alias cii="export INIT_MAMBA=true && init_mamba"
+

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.

+
set fish_color_command cyan
+set fish_color_comment green
+set fish_color_end black
+set fish_color_error red
+set fish_color_escape yellow
+set fish_color_operator yellow
+set fish_color_param magenta
+set fish_color_quote green
+set fish_color_redirection yellow
+

Keybindings

+
bind -M insert \el forward-char
+bind -M insert \eh backward-char
+bind -M insert \ew forward-word
+bind -M insert \eb backward-word
+

Functions

+

A small function to open the file with $EDITOR.

+
function e
+    eval $EDITOR $argv
+end
+

direnv

+
direnv hook fish | source
+

Nushell

+ + + + + + + + + + + +
Guix dependency
nushell-bin
+

A structured shell. I don’t use it as of now, but perhaps one day.

+

Starship prompt

+ + + + + + + + + + + + + +
Guix dependencyDescription
starship-binmy prompt of choice
+

Starship is a nice cross-shell prompt, written in Rust.

+

References:

+ + +
[character]
+success_symbol = "[➤ ](bold green)"
+error_symbol = "[ ](bold red)"
+vicmd_symbol = "[ᐊ ](bold green)"
+
+[aws]
+symbol = " "
+
+# [battery]
+# full_symbol = ""
+# charging_symbol = ""
+# discharging_symbol = ""
+
+[conda]
+symbol = " "
+
+[cmd_duration]
+min_time = 500
+format = " [$duration]($style) "
+
+[docker_context]
+symbol = " "
+
+[elixir]
+symbol = " "
+
+[elm]
+symbol = " "
+
+[git_branch]
+symbol = " "
+truncation_length = 20
+
+[golang]
+symbol = " "
+
+# [haskell]
+# symbol = " "
+
+[hg_branch]
+symbol = " "
+
+[java]
+symbol = " "
+
+[julia]
+symbol = " "
+
+[memory_usage]
+symbol = " "
+
+[nim]
+symbol = " "
+
+[nix_shell]
+symbol = " "
+
+[nodejs]
+symbol = " "
+
+[package]
+symbol = " "
+disabled = true
+
+[php]
+symbol = " "
+
+[python]
+symbol = " "
+
+[ruby]
+symbol = " "
+
+[rust]
+symbol = " "
+

Tmux

+ + + + + + + + + + + + + + +
Guix dependency
tmux
python-tmuxp
+

tmux is my terminal multiplexer of choice.

+

It provides pretty sane defaults, so the config is not too large. I rebind the prefix to C-a though.

+

Term settings

+

I have no idea how and why these statements work.

+
set -g default-terminal "screen-256color"
+set -ga terminal-overrides ",*256col*:Tc"
+

History limit.

+
set -g history-limit 20000
+

Keybindings

+

Enable vi keys and mouse.

+
set-window-option -g mode-keys vi
+set-option -g xterm-keys on
+set-option -g mouse on
+set -sg escape-time 10
+

Change prefix from C-b to C-a.

+
unbind C-b
+set -g prefix C-a
+bind C-a send-prefix
+

Vi-like keybindings to manage panes & windows.

+
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
+

Reload the config.

+
bind r source-file ~/.tmux.conf
+

Copy to clipboard

+ + + + + + + + + + + +
Guix dependency
xclip
+

Make tmux copy to clipboard as well

+
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "xclip -selection clipboard -i"
+bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "xclip -selection clipboard -i"
+

UI

+

On [2020-01-22 Wed], I had generated the first version of this following with tmuxline.vim plugin and palenight theme for vim-airline.

+

Then I adapted it to use the current Emacs theme.

+
set -g status-justify "centre"
+set -g status "on"
+set -g status-left-style "none"
+set -g message-command-style "fg=<<get-color(name="fg")>>,bg=<<get-color(name="bg-other")>>"
+set -g status-right-style "none"
+set -g pane-active-border-style "fg=<<get-color(name="blue")>>"
+set -g status-style "none,bg=<<get-color(name="bg-other")>>"
+set -g message-style "fg=<<get-color(name="fg")>>,bg=<<get-color(name="bg-other")>>"
+set -g pane-border-style "fg=<<get-color(name="blue")>>"
+set -g status-right-length "100"
+set -g status-left-length "100"
+setw -g window-status-activity-style "none,fg=<<get-color(name="blue")>>,bg=<<get-color(name="bg-other")>>"
+setw -g window-status-separator ""
+setw -g window-status-style "none,fg=<<get-color(name="fg")>>,bg=<<get-color(name="bg-other")>>"
+set -g status-left "#[fg=<<get-fg-for-color(name="blue")>>,bg=<<get-color(name="blue")>>] #S #[fg=<<get-color(name="blue")>>,bg=<<get-color(name="light-blue")>>,nobold,nounderscore,noitalics]#[fg=<<get-fg-for-color(name="light-blue")>>,bg=<<get-color(name="light-blue")>>] #W #[fg=<<get-color(name="light-blue")>>,bg=<<get-color(name="bg-other")>>,nobold,nounderscore,noitalics]"
+set -g status-right "%-H:%M #[bg=<<get-color(name="bg-other")>>,fg=<<get-color(name="light-blue")>>,nobold,nounderscore,noitalics]#[fg=<<get-fg-for-color(name="light-blue")>>,bg=<<get-color(name="light-blue")>>] %a, %b %d #[bg=<<get-color(name="light-blue")>>,fg=<<get-color(name="blue")>>,nobold,nounderscore,noitalics]#[fg=<<get-fg-for-color(name="blue")>>,bg=<<get-color(name="blue")>>] #H "
+setw -g window-status-format "#[fg=<<get-color(name="bg-other")>>,bg=<<get-color(name="light-blue")>>,nobold,nounderscore,noitalics]#[fg=<<get-fg-for-color(name="light-blue")>>,bg=<<get-color(name="light-blue")>>] #I #W #[align=left] #[fg=<<get-color(name="light-blue")>>,bg=<<get-color(name="bg-other")>>,nobold,nounderscore,noitalics]"
+setw -g window-status-current-format "#[fg=<<get-color(name="bg-other")>>,bg=<<get-color(name="blue")>>,nobold,nounderscore,noitalics]#[fg=<<get-fg-for-color(name="blue")>>,bg=<<get-color(name="blue")>>] #I #W #[fg=<<get-color(name="blue")>>,bg=<<get-color(name="bg-other")>>,nobold,nounderscore,noitalics]"
+

Source the line config:

+
source ~/.tmux.line.conf
+

Alacritty

+ + + + + + + + + + + +
Guix dependency
alacritty
+

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.

+

References:

+ + +
decorations: none
+
+font:
+  normal:
+    family: JetBrainsMono Nerd Font
+    style: Regular
+
+  size: 10
+
+env:
+  TERM: xterm-256color
+
+colors:
+  primary:
+    background: '<<get-color(name="bg")>>'
+    foreground: '<<get-color(name="fg")>>'
+  normal:
+    black: '<<get-color(name="black")>>'
+    red: '<<get-color(name="red")>>'
+    green: '<<get-color(name="green")>>'
+    yellow: '<<get-color(name="yellow")>>'
+    blue: '<<get-color(name="blue")>>'
+    magenta: '<<get-color(name="magenta")>>'
+    cyan: '<<get-color(name="cyan")>>'
+    white: '<<get-color(name="white")>>'
+  bright:
+    Black: '<<get-color(name="light-black")>>'
+    Red: '<<get-color(name="light-red")>>'
+    Green: '<<get-color(name="light-green")>>'
+    Yellow: '<<get-color(name="light-yellow")>>'
+    Blue: '<<get-color(name="light-blue")>>'
+    Magenta: '<<get-color(name="light-magenta")>>'
+    Cyan: '<<get-color(name="light-cyan")>>'
+    White: '<<get-color(name="light-white")>>'
+
+window:
+  padding:
+    x: 0
+    y: 0
+  dynamic_padding: true
+  opacity: 1
+
+key_bindings:
+  - { key: Paste,                                       action: Paste          }
+  - { key: Copy,                                        action: Copy           }
+  - { key: L,         mods: Control,                    action: ClearLogNotice }
+  - { key: L,         mods: Control, mode: ~Vi|~Search, chars: "\x0c"          }
+  - { key: PageUp,    mods: Shift,   mode: ~Alt,        action: ScrollPageUp,  }
+  - { key: PageDown,  mods: Shift,   mode: ~Alt,        action: ScrollPageDown }
+  - { key: Home,      mods: Shift,   mode: ~Alt,        action: ScrollToTop,   }
+  - { key: End,       mods: Shift,   mode: ~Alt,        action: ScrollToBottom }
+
+  #  Turn off vi mode
+  - { key: Space,  mods: Shift|Control, mode: ~Search,    action: ReceiveChar             }
+
+  # (Windows, Linux, and BSD only)
+  - { key: V,              mods: Control|Shift, mode: ~Vi,        action: Paste            }
+  - { key: C,              mods: Control|Shift,                   action: Copy             }
+  - { key: F,              mods: Control|Shift, mode: ~Search,    action: ReceiveChar    }
+  - { key: B,              mods: Control|Shift, mode: ~Search,    action: ReceiveChar   }
+  - { key: Insert,         mods: Shift,                           action: PasteSelection   }
+  - { key: Key0,           mods: Control,                         action: ResetFontSize    }
+  - { key: Equals,         mods: Control,                         action: IncreaseFontSize }
+  - { key: Plus,           mods: Control,                         action: IncreaseFontSize }
+  - { key: NumpadAdd,      mods: Control,                         action: IncreaseFontSize }
+  - { key: Minus,          mods: Control,                         action: DecreaseFontSize }
+  - { key: NumpadSubtract, mods: Control,                         action: DecreaseFontSize }
+

Bottom

+ + + + + + + + + + + + + +
Guix dependencyDescription
bottom-binresource monitor
+

bottom is a TUI system monitor.

+

See the default config for the avaliable options.

+

+
(if (my/light-p) "default-light" "default")
+
[flags]
+hide_table_gap = true  # Remove space in tables
+color = "<<bottom-theme()>>"
+process_command = true
+
+[processes]
+columns = ["PID", "State", "Name", "CPU%", "Mem%", "R/s", "W/s", "User"]
+

Various console applications

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Guix dependencyDescription
ncursesProvides stuff like clear
exals replacement, written in Rust
batcat clone with syntax highlighting
htopInteractive process viewer
nethogsA tool to group processed by used bandwidth
osyncrsync wrapper
neofetchFetch system info
fzffuzzy finder
p7ziparchiver
password-storeCLI password manager
zip
unzip
jmtpfsA tool to mount MTP devices (e.g. Android)
tokeiCount lines of code
sshfsMount stuff over SSH
lftpFile transfer
git-lfs
glibc-locales
direnv
jless-binJSON viewer
megacmdmega.nz client
ncdudisk usage analyzer
openssl
+

ripgrep config

+

Occasionally I can’t exclude certain files from ripgrep via the VCS settings, so here is a simple config to ignore certain files globally.

+
--ignore-file=/home/pavel/.config/ripgrep/ripgrepignore
+

The ignore file:

+
*.ts.snap
+

By default, ripgrep doesn’t read any configs, so it is necessary to set the RIPGREP_CONFIG_PATH variable in the .profile.

+

Misc scripts

+

nt - exec command with a finished notification

+

Usage:

+
nt <command>
+
command="$@"
+if [ ! -z "$command" ]; then
+    start_time="$(date -u +%s)"
+    $command
+    end_time="$(date -u +%s)"
+    elapsed="$(($end_time-$start_time))"
+    notify-send "Terminal" "Command\n$command\nexecuted in $elapsed seconds"
+else
+    notify-send "Terminal" "Command execution complete"
+fi
+

autocommit

+

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:

+ + + + + + + + + + + + + + + +
VariableDescriptionDefault value
TIMEOUT_MINDefault timeout60
+

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
  • +
+ +
TIMEOUT_MIN=${TIMEOUT_MIN:-60}
+
+export DISPLAY=:0
+cd "$1"
+
+TIMESTAMP=$(date +%s)
+LAST_COMMIT_TIMESTAMP=$(git log -1 --format="%at" | xargs -I{} date -d @{} +%s)
+RECENTLY_CHANGED_NUM=$(find . -not -path '*/\.*' -mmin -$TIMEOUT_MIN | wc -l)
+CHANGED_NUM=$(git status --porcelain | wc -l)
+COMMITED="No"
+PUSHED="No"
+FETCHED="No"
+MERGED="No"
+
+notify () {
+    if command -v notify-send; then
+	notify-send -u ${LEVEL:-normal} "$1" "$2"
+    else
+	echo "$1" "$2"
+    fi
+}
+
+if [[ $(git ls-files -u | wc -l) -gt 0 ]]; then
+    LEVEL=critical notify "Autocommit $(pwd)" "Merge conflict!"
+fi
+
+if [[ ($RECENTLY_CHANGED_NUM -eq 0 || $2 = "-F") && $CHANGED_NUM -gt 0 ]]; then
+    read -r -d '' MESSAGE << EOM
+Autocommit $(date -Iminutes)
+
+Hostname: $(hostname)
+EOM
+    git add -A
+    git commit -m "$MESSAGE"
+    COMMITED="Yes"
+fi
+
+NEED_TO_PUSH=$(git log origin/master..HEAD | wc -l)
+
+git fetch && FETCHED="Yes" || FETCHED="No"
+if [[ $RECENTLY_CHANGED_NUM -gt 0 && $2 != '-F' ]]; then
+    MERGED="Waiting"
+fi
+
+if [[ ($RECENTLY_CHANGED_NUM -eq 0 || $2 = "-F") && $FETCHED = "Yes" ]]; then
+    MERGE_OUT=$(git merge origin/master) && MERGED="Yes" || MERGED="No"
+fi
+
+if [[ $NEED_TO_PUSH -gt 0 && ($MERGED = "Yes" || $MERGED = "Waiting") ]]; then
+    git push origin && PUSHED="Yes" || PUSHED="No"
+fi
+
+if [[ $PUSHED = "Yes" || $COMMITED = "Yes" || ($MERGED = "Yes" &&  $MERGE_OUT != "Already up to date.")]]; then
+    read -r -d '' NOTIFICATION << EOM
+Commited: $COMMITED
+Fetched: $FETCHED
+Merged: $MERGED
+Pushed: $PUSHED
+EOM
+    notify "Autocommit $(pwd)" "$NOTIFICATION"
+fi
+
+if [[ $(git ls-files -u | wc -l) -gt 0 ]]; then
+    LEVEL=critical notify "Autocommit $(pwd)" "Merge conflict!"
+fi
+

mcron job:

+
(job "0 * * * *" "autocommit ~/Documents/org-mode")
+(job "0,15,30,45 * * * *" "autocommit ~/.password-store")
+

Guix settings

+

+
(my/format-guix-dependencies)
+
(specifications->manifest
+ '(
+   <<packages()>>))
+

Android notes

+

SSH instructions: https://wiki.termux.com/wiki/Remote_Access

+

Don’t forget to install the following termux packages:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Termux package
vim
tmux
starship
fish
exa
bat
git
+

Also:

+
    +
  • cleanup $PREFIX/etc/motd to remove hello message.
  • +
  • copy the required font at $HOME/.termux/font.ttf and run termux-reload-settings.
  • +
+

Installation of DT’s colorscripts:

+
git clone https://gitlab.com/dwt1/shell-color-scripts.git
+cd shell-color-scripts
+

Apply a patch:

+
--- a/colorscript.sh
++++ b/colorscript.sh
+@@ -2,7 +2,7 @@
+
+ # Simple CLI for shell-color-scripts
+
+-DIR_COLORSCRIPTS="/opt/shell-color-scripts/colorscripts"
++DIR_COLORSCRIPTS="$PREFIX/opt/shell-color-scripts/colorscripts"
+ LS_CMD="$(command -v ls)"
+ fmt_help="  %-20s\t%-54s\n"
+ list_colorscripts="$($LS_CMD "${DIR_COLORSCRIPTS}" | cut -d ' ' -f 1 | nl)"
+
sudo mkdir -p $PREFIX/opt/shell-color-scripts/colorscripts || return 1
+sudo cp -rf colorscripts/* $PREFIX/opt/shell-color-scripts/colorscripts
+sudo cp colorscript.sh $PREFIX/bin/colorscript
+
+
+ +
+ +
+ + diff --git a/configs/desktop/index.html b/configs/desktop/index.html new file mode 100644 index 0000000..0ea3143 --- /dev/null +++ b/configs/desktop/index.html @@ -0,0 +1,4645 @@ + + + + + + Desktop + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ Desktop + +

+
+

+ 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:

+ +

Some remarks

+

Removed features:

+ + + + + + + + + + + + + +
FeatureLast commit
rofi-bukue22476b0cc6315e104e5ce4de5559a61c830c429
+

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.

+

To use them, let’s define a noweb block:

+

+
(let ((color (or (my/color-value name))))
+  (if (> quote 0)
+      (concat "\"" color "\"")
+    color))
+

Test:

+
<<get-color(name="red", quote=1)>>
+

Also, get a foreground for the current 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))
+

Test;

+
<<get-fg-for-color(name="red", quote=1)>>
+

This table used to have values, now it has only keys:

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
colorkey
blackcolor0
redcolor1
greencolor2
yellowcolor3
bluecolor4
magentacolor5
cyancolor6
whitecolor7
light-blackcolor8
light-redcolor9
light-greencolor10
light-yellowcolor11
light-bluecolor12
light-magentacolor13
light-cyancolor14
light-whitecolor15
+

Xresources

+

Colors in Xresources

+

Some programs get their colors from XResources. Let’s generate that file.

+

+
(mapconcat
+ (lambda (elem)
+   (concat "*" (nth 1 elem) ": " (my/color-value (nth 0 elem))))
+ (seq-filter
+  (lambda (elem) (and (nth 1 elem)
+		      (not (string-empty-p (nth 1 elem)))))
+  table)
+ "\n")
+
<<get-xresources()>>
+
+*background: <<get-color(name="bg")>>
+*foreground: <<get-color(name="fg")>>
+

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.

+

+
(let ((hostname (system-name)))
+  (cond ((string-equal hostname "azure") 120)
+	((string-equal hostname "eminence") 120)
+	((string-equal hostname "indigo") 120)
+	((string-equal hostname "iris") 120)
+	(t 96)))
+
Xft.dpi: <<get-dpi()>>
+

Themes

+

A few programs I use to customize the apperance are listed below.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Guix dependencyDescription
matcha-themeMy preferred GTK theme
papirus-icon-themeMy preferred Icon theme
gnome-themes-standard
xsettingsdX11 settings daemon
gnome-themes-extra
+

xsettingsd is a lightweight daemon which configures X11 applications. It is launched with shepherd in the Services section.

+

+
(if (my/light-p)
+    "Matcha-light-azul"
+  "Matcha-dark-azul")
+

+
(if (my/light-p)
+    "Papirus"
+  "Papirus-Dark")
+
Net/ThemeName "<<get-gtk-theme()>>"
+Net/IconThemeName "<<get-icons-theme()>>"
+Gtk/DecorationLayout "menu:minimize,maximize,close"
+Gtk/FontName "Sans 10"
+Gtk/MonospaceFontName "JetBrainsMono Nerd Mono 12"
+Gtk/CursorThemeName "Adwaita"
+Xft/Antialias 1
+Xft/Hinting 0
+Xft/HintStyle "hintnone"
+

MIME

+

Setting the default MIME types

+
[Default Applications]
+text/html=firefox.desktop
+x-scheme-handler/http=firefox.desktop
+x-scheme-handler/https=firefox.desktop
+x-scheme-handler/about=firefox.desktop
+x-scheme-handler/unknown=firefox.desktop
+x-scheme-handler/tg=userapp-Telegram Desktop-7PVWF1.desktop
+image/png=feh.desktop
+image/jpg=feh.desktop
+image/jpeg=feh.desktop
+application/pdf=org.pwmt.zathura.desktop
+
+[Added Associations]
+x-scheme-handler/tg=userapp-Telegram Desktop-7PVWF1.desktop;
+application/pdf=org.pwmt.zathura.desktop
+

Device-specific settings

+ + + + + + + + + + + + + + + + + + + + + +
Guix dependencyDescription
xrandrX11 CLI to RandR
xgammaA tool to alter monitor’s gamma correction
xinputConfigure input devices
+

Set screen layout & other params depending on hostname

+
hostname=$(hostname)
+if [ "$hostname" = "indigo" ]; then
+    xrandr --output DisplayPort-0 --off --output HDMI-A-0 --mode 1920x1080 --pos 0x0 --rotate normal --output DVI-D-0 --mode 1920x1080 --pos 1920x0 --rotate normal
+elif [ "$hostname" = "eminence" ]; then
+    xgamma -gamma 1.25
+fi
+

EXWM

+

Settings for Emacs X Window Manager, a tiling WM implemented in Emacs Lisp. This part has a few bits copied from my blog post.

+

References:

+ +

Startup & UI

+

Xsession

+

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.

+ + + + + + + + + + + + + + + + + +
CategoryGuix dependency
desktop-miscxinit
desktop-miscxss-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.
  • +
+

As of now, these are polybar, feh and, shepherd:

+
(defun my/exwm-run-polybar ()
+  (call-process "~/bin/polybar.sh"))
+
+(defun my/exwm-set-wallpaper ()
+  (call-process-shell-command "feh --bg-fill ~/Pictures/wallpaper.jpg"))
+
+(defun my/exwm-run-shepherd ()
+  (when (string-empty-p (shell-command-to-string "pgrep -u pavel shepherd"))
+    (call-process "shepherd")))
+

Pinentry

+

The GUI pinentry doesn’t work too well with EXWM because of issues with popup windows, so we will use the Emacs one.

+
(use-package pinentry
+  :straight t
+  :after (exwm)
+  :config
+  (setenv "GPG_AGENT_INFO" nil) ;; use emacs pinentry
+  (setq auth-source-debug t)
+
+  (setq epg-gpg-program "gpg2") ;; not necessary
+  (require 'epa-file)
+  (epa-file-enable)
+  (setq epa-pinentry-mode 'loopback)
+  (setq epg-pinentry-mode 'loopback)
+  (pinentry-start))
+
default-cache-ttl 3600
+max-cache-ttl 3600
+allow-emacs-pinentry
+allow-loopback-pinentry
+pinentry-program /home/pavel/.guix-extra-profiles/console/console/bin/pinentry
+

Modeline

+

Show the current workspace in the modeline.

+
(use-package exwm-modeline
+  :straight t
+  :config
+  (add-hook 'exwm-init-hook #'exwm-modeline-mode)
+  (my/use-colors
+   (exwm-modeline-current-workspace
+    :foreground (my/color-value 'yellow)
+    :weight 'bold)))
+

Windows

+

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)
+
+(defun my/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-window dir)))
+	       (and win (not (window-minibuffer-p win)))))
+	   (pcase dir
+	     ('width '(left right))
+	     ('height '(up down)))))
+

And a function to implement that:

+
(defun my/exwm-move-window (dir)
+  "Move the current window in the direction DIR."
+  (let ((other-window (windmove-find-other-window dir))
+	(other-direction (my/exwm-direction-exists-p
+			  (pcase dir
+			    ('up 'width)
+			    ('down 'width)
+			    ('left 'height)
+			    ('right 'height)))))
+    (cond
+     ((and other-window (not (window-minibuffer-p other-window)))
+      (window-swap-states (selected-window) other-window))
+     (other-direction
+      (evil-move-window dir)))))
+

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:

+
(setq my/exwm-resize-value 5)
+
+(defun my/exwm-resize-window (dir kind &optional value)
+  "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."
+  (unless value
+    (setq value my/exwm-resize-value))
+  (let* ((is-exwm-floating
+	  (and (derived-mode-p 'exwm-mode)
+	       exwm--floating-frame))
+	 (func (if is-exwm-floating
+		   (intern
+		    (concat
+		     "exwm-layout-"
+		     (pcase kind ('shrink "shrink") ('grow "enlarge"))
+		     "-window"
+		     (pcase dir ('height "") ('width "-horizontally"))))
+		 (intern
+		  (concat
+		   "evil-window"
+		   (pcase kind ('shrink "-decrease-") ('grow "-increase-"))
+		   (symbol-name dir))))))
+    (when is-exwm-floating
+      (setq value (* 5 value)))
+    (funcall func value)))
+

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:

+
(defhydra my/exwm-resize-hydra (:color pink :hint nil :foreign-keys run)
+  "
+^Resize^
+_l_: Increase width   _h_: Decrease width   _j_: Increase height   _k_: Decrease height
+
+_=_: Balance          "
+  ("h" (lambda () (interactive) (my/exwm-resize-window 'width 'shrink)))
+  ("j" (lambda () (interactive) (my/exwm-resize-window 'height 'grow)))
+  ("k" (lambda () (interactive) (my/exwm-resize-window 'height 'shrink)))
+  ("l" (lambda () (interactive) (my/exwm-resize-window 'width 'grow)))
+  ("=" balance-windows)
+  ("q" nil "quit" :color blue))
+

Improving splitting windows

+

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:

+
(defun my/exwm-fill-other-window (&rest _)
+  "Open the most recently used buffer in the next window."
+  (interactive)
+  (when (and (eq major-mode 'exwm-mode) (not (eq (next-window) (get-buffer-window))))
+    (let ((other-exwm-buffer
+	   (cl-loop with other-buffer = (persp-other-buffer)
+		    for buf in (sort (persp-current-buffers) (lambda (a _) (eq a other-buffer)))
+		    with current-buffer = (current-buffer)
+		    when (and (not (eq current-buffer buf))
+			      (buffer-live-p buf)
+			      (not (string-match-p (persp--make-ignore-buffer-rx) (buffer-name buf)))
+			      (not (get-buffer-window buf)))
+		    return buf)))
+      (when other-exwm-buffer
+	(with-selected-window (next-window)
+	  (switch-to-buffer other-exwm-buffer))))))
+

This is meant to be called after doing an either vertical or horizontal split, so it’s advised like that:

+
(advice-add 'evil-window-split :after #'my/exwm-fill-other-window)
+(advice-add 'evil-window-vsplit :after #'my/exwm-fill-other-window)
+

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.
  • +
+

References:

+ + +
(use-package perspective-exwm
+  :straight t
+  :config
+  (setq perspective-exwm-override-initial-name
+	'((0 . "misc")
+	  (1 . "core")
+	  (2 . "browser")
+	  (3 . "comms")
+	  (4 . "dev")))
+  (setq perspective-exwm-cycle-max-message-length 180)
+  (general-define-key
+   :keymaps 'perspective-map
+   "e" #'perspective-exwm-move-to-workspace
+   "E" #'perspective-exwm-copy-to-workspace))
+

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:

+
(defun my/exwm-configure-window ()
+  (interactive)
+  (unless exwm--floating-frame
+    (pcase exwm-class-name
+      ((or "Firefox" "Nightly")
+       (perspective-exwm-assign-window
+	:workspace-index 2
+	:persp-name "browser"))
+      ("Nyxt"
+       (perspective-exwm-assign-window
+	:workspace-index 2
+	:persp-name "browser"))
+      ("Alacritty"
+       (perspective-exwm-assign-window
+	:persp-name "term"))
+      ((or "VK" "Slack" "discord" "TelegramDesktop" "Rocket.Chat")
+       (perspective-exwm-assign-window
+	:workspace-index 3
+	:persp-name "comms"))
+      ((or "Chromium-browser" "jetbrains-datagrip")
+       (perspective-exwm-assign-window
+	:workspace-index 4
+	:persp-name "dev")))))
+
+(add-hook 'exwm-manage-finish-hook #'my/exwm-configure-window)
+

Workspaces and multi-monitor setup

+

A section about improving management of EXWM workspaces.

+

Some features, common in other tiling WMs, are missing in EXWM out of the box, namely:

+ +

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:

+
(setq my/exwm-last-workspaces '(1))
+
+(defun my/exwm-store-last-workspace ()
+  "Save the last workspace to `my/exwm-last-workspaces'."
+  (setq my/exwm-last-workspaces
+	(seq-uniq (cons exwm-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:

+
(defun my/exwm-last-workspaces-clear ()
+  "Clean `my/exwm-last-workspaces' from deleted workspaces."
+  (setq my/exwm-last-workspaces
+	(seq-filter
+	 (lambda (i) (nth i exwm-workspace--list))
+	 my/exwm-last-workspaces)))
+

The monitor 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:

+
(setq my/exwm-monitor-list
+      (pcase (system-name)
+	("indigo" '(nil "DVI-D-0"))
+	(_ '(nil))))
+

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:

+
(defun my/exwm-get-current-monitor ()
+  "Return the current monitor name or nil."
+  (plist-get exwm-randr-workspace-output-plist
+	     (cl-position (selected-frame)
+			  exwm-workspace--list)))
+

And a function to cycle the monitor list in either direction:

+
(defun my/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)
+	 (length my/exwm-monitor-list)
+	 (pcase dir
+	   ('right 1)
+	   ('left -1)))
+      (length my/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.

+
(defun my/exwm-switch-to-other-monitor (&optional dir)
+  "Switch to another monitor."
+  (interactive)
+  (my/exwm-last-workspaces-clear)
+  (let ((mouse-autoselect-window nil))
+    (exwm-workspace-switch
+     (cl-loop with other-monitor = (my/exwm-get-other-monitor (or dir 'right))
+	      for i in (append my/exwm-last-workspaces
+			       (cl-loop for i from 0
+					for _ in exwm-workspace--list
+					collect i))
+	      if (if other-monitor
+		     (string-equal (plist-get exwm-randr-workspace-output-plist i)
+				   other-monitor)
+		   (not (plist-get exwm-randr-workspace-output-plist i)))
+	      return i))))
+

I bind this function to s-q, as I’m used from i3.

+

Move the workspace to another monitor

+

Now, moving the workspace to another monitor.

+

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.

+
(defun my/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 (and current-monitor
+	       (>= 1
+		   (cl-loop for (key value) on exwm-randr-workspace-monitor-plist
+			    by 'cddr
+			    if (string-equal value current-monitor) sum 1)))
+      (error "Can't remove the last workspace on the monitor!"))
+    (setq exwm-randr-workspace-monitor-plist
+	  (map-delete exwm-randr-workspace-monitor-plist exwm-workspace-current-index))
+    (when new-monitor
+      (setq exwm-randr-workspace-monitor-plist
+	    (plist-put exwm-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.

+
(defun my/exwm-windmove (dir)
+  "Move to window or monitor in the direction DIR."
+  (if (or (eq dir 'down) (eq dir 'up))
+      (windmove-do-window-select dir)
+    (let ((other-window (windmove-find-other-window dir))
+	  (other-monitor (my/exwm-get-other-monitor dir))
+	  (opposite-dir (pcase dir
+			  ('left 'right)
+			  ('right 'left))))
+      (if other-window
+	  (windmove-do-window-select dir)
+	(let ((mouse-autoselect-window nil))
+	  (my/exwm-switch-to-other-monitor dir))
+	(cl-loop while (windmove-find-other-window opposite-dir)
+		 do (windmove-do-window-select opposite-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.

+
(use-package ivy-posframe
+  :straight t
+  :disabled
+  :config
+  (setq ivy-posframe-parameters '((left-fringe . 10)
+				  (right-fringe . 10)
+				  (parent-frame . nil)
+				  (max-width . 80)))
+  (setq ivy-posframe-height-alist '((t . 20)))
+  (setq ivy-posframe-width 180)
+  (setq ivy-posframe-min-height 5)
+  (setq ivy-posframe-display-functions-alist
+	'((swiper . ivy-display-function-fallback)
+	  (swiper-isearch . ivy-display-function-fallback)
+	  (t . ivy-posframe-display)))
+  (ivy-posframe-mode 1))
+
Disable mouse movement
+

SOURCE: https://github.com/ch11ng/exwm/issues/550#issuecomment-744784838

+
(defun my/advise-fn-suspend-follow-mouse (fn &rest args)
+  (let ((focus-follows-mouse nil)
+	(mouse-autoselect-window nil)
+	(pos (x-mouse-absolute-pixel-position)))
+    (unwind-protect
+	(apply fn args)
+      (x-set-mouse-absolute-pixel-position (car pos)
+					   (cdr pos)))))
+(with-eval-after-load 'ivy-posframe
+  (advice-add #'ivy-posframe--read :around #'my/advise-fn-suspend-follow-mouse))
+
Disable changing focus
+

Not sure about that. The cursor occasionally changes focus when I’m exiting posframe, and this doesn’t catch all the cases.

+
(defun my/setup-posframe (&rest args)
+  (mapc
+   (lambda (var)
+     (kill-local-variable var)
+     (setf (symbol-value var) nil))
+   '(exwm-workspace-warp-cursor
+     mouse-autoselect-window
+     focus-follows-mouse)))
+
+(defun my/restore-posframe (&rest args)
+  (run-with-timer
+   0.25
+   (lambda ()
+     (mapc
+      (lambda (var)
+	(kill-local-variable var)
+	(setf (symbol-value var) t))
+      '(exwm-workspace-warp-cursor
+	mouse-autoselect-window
+	focus-follows-mouse)))))
+
+(with-eval-after-load 'ivy-posframe
+  (advice-add #'posframe--create-posframe :after #'my/setup-posframe)
+  (advice-add #'ivy-posframe-cleanup :after #'my/restore-posframe))
+

Linux app

+

counsel-linux-app is a counsel interface to select a Linux desktop application.

+

By default, it also shows paths from /gnu/store, so there is a custom formatter function.

+
(defun my/counsel-linux-app-format-function (name comment _exec)
+  (format "% -45s%s"
+	  (propertize
+	   (ivy--truncate-string name 45)
+	   'face 'counsel-application-name)
+	  (if comment
+	      (concat ": " (ivy--truncate-string comment 100))
+	    "")))
+
+(setq counsel-linux-app-format-function #'my/counsel-linux-app-format-function)
+

Also, by default it tries to launch stuff with gtk-launch, which is in the gtk+ package.

+ + + + + + + + + + + + + +
CategoryGuix dependency
desktop-miscgtk+:bin
+

password-store-ivy

+

password-store-ivy is another package of mine, inspired by rofi-pass.

+
(use-package password-store-ivy
+  :straight (:host github :repo "SqrtMinusOne/password-store-ivy")
+  :after (exwm))
+

emojis

+

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-package emojify
+  :straight t)
+

Because I occasionally want to type emojis to other programs, I reuse a function from password-store-ivy:

+
(defun my/emojify-type ()
+  "Type an emoji."
+  (interactive)
+  (let ((emoji (emojify-completing-read "Type emoji: ")))
+    (kill-new emoji)
+    (password-store-ivy--async-commands
+     (list
+      (password-store-ivy--get-wait-command 10)
+      "xdotool key Shift+Insert"))))
+

Keybindings

+

EXWM keybindings

+

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:

+
(setq exwm-input-prefix-keys
+      `(?\C-x
+	?\C-w
+	?\M-x
+	?\M-u))
+

Also other local keybindings, that are also available only in line-mode:

+
(defmacro my/app-command (command)
+  `(lambda () (interactive) (my/run-in-background ,command)))
+
+(general-define-key
+ :keymaps '(exwm-mode-map)
+ "C-q" #'exwm-input-send-next-key
+ "<print>" (my/app-command "flameshot gui")
+ "<mode-line> s-<mouse-4>" #'perspective-exwm-cycle-all-buffers-backward
+ "<mode-line> s-<mouse-5>" #'perspective-exwm-cycle-all-buffers-forward
+ "M-x" #'counsel-M-x
+ "M-SPC" (general-key "SPC"))
+

Simulation keys.

+
(setq exwm-input-simulation-keys `((,(kbd "M-w") . ,(kbd "C-w"))
+				   (,(kbd "M-c") . ,(kbd "C-c"))))
+

A quit function with a confirmation.

+
(defun my/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:

+
(setq exwm-input-global-keys
+      `(
+	;; Reset to line-mode
+	(,(kbd "s-R") . exwm-reset)
+
+	;; Switch windows
+	(,(kbd "s-<left>") . (lambda () (interactive) (my/exwm-windmove 'left)))
+	(,(kbd "s-<right>") . (lambda () (interactive) (my/exwm-windmove 'right)))
+	(,(kbd "s-<up>") . (lambda () (interactive) (my/exwm-windmove 'up)))
+	(,(kbd "s-<down>") . (lambda () (interactive) (my/exwm-windmove 'down)))
+
+	(,(kbd "s-h"). (lambda () (interactive) (my/exwm-windmove 'left)))
+	(,(kbd "s-l") . (lambda () (interactive) (my/exwm-windmove 'right)))
+	(,(kbd "s-k") . (lambda () (interactive) (my/exwm-windmove 'up)))
+	(,(kbd "s-j") . (lambda () (interactive) (my/exwm-windmove 'down)))
+
+	;; Moving windows
+	(,(kbd "s-H") . (lambda () (interactive) (my/exwm-move-window 'left)))
+	(,(kbd "s-L") . (lambda () (interactive) (my/exwm-move-window 'right)))
+	(,(kbd "s-K") . (lambda () (interactive) (my/exwm-move-window 'up)))
+	(,(kbd "s-J") . (lambda () (interactive) (my/exwm-move-window 'down)))
+
+	;; Fullscreen
+	(,(kbd "s-f") . exwm-layout-toggle-fullscreen)
+	(,(kbd "s-F") . exwm-floating-toggle-floating)
+
+	;; Quit
+	(,(kbd "s-Q") . my/exwm-quit)
+
+	;; Split windows
+	(,(kbd "s-s") . evil-window-vsplit)
+	(,(kbd "s-v") . evil-window-hsplit)
+
+	;; Switch perspectives
+	(,(kbd "s-,") . persp-prev)
+	(,(kbd "s-.") . persp-next)
+
+	;; Switch buffers
+	(,(kbd "s-e") . persp-ivy-switch-buffer)
+	(,(kbd "s-E") . my/persp-ivy-switch-buffer-other-window)
+
+	;; Resize windows
+	(,(kbd "s-r") . my/exwm-resize-hydra/body)
+
+	;; Apps & stuff
+	(,(kbd "s-p") . counsel-linux-app)
+	(,(kbd "s-P") . async-shell-command)
+	(,(kbd "s-;") . my/exwm-apps-hydra/body)
+	(,(kbd "s--") . password-store-ivy)
+	(,(kbd "s-=") . my/emojify-type)
+	(,(kbd "s-i") . ,(my/app-command "copyq menu"))
+
+	;; Basic controls
+	(,(kbd "<XF86AudioRaiseVolume>") . ,(my/app-command "ponymix increase 5 --max-volume 150"))
+	(,(kbd "<XF86AudioLowerVolume>") . ,(my/app-command "ponymix decrease 5 --max-volume 150"))
+	(,(kbd "<XF86MonBrightnessUp>") . ,(my/app-command "light -A 5"))
+	(,(kbd "<XF86MonBrightnessDown>") . ,(my/app-command "light -U 5"))
+	(,(kbd "<XF86AudioMute>") . ,(my/app-command "ponymix toggle"))
+
+	(,(kbd "<XF86AudioPlay>") . ,(my/app-command "mpc toggle"))
+	(,(kbd "<XF86AudioPause>") . ,(my/app-command "mpc pause"))
+	(,(kbd "<print>") . ,(my/app-command "flameshot gui"))
+
+	;; Switch workspace
+	(,(kbd "s-q") . my/exwm-switch-to-other-monitor)
+	(,(kbd "s-w") . exwm-workspace-switch)
+	(,(kbd "s-W") . exwm-workspace-move-window)
+	(,(kbd "s-<tab>") . my/exwm-workspace-switch-monitor)
+
+	;; Perspectives
+	(,(kbd "s-{") . perspective-exwm-cycle-all-buffers-backward)
+	(,(kbd "s-}") . perspective-exwm-cycle-all-buffers-forward)
+	(,(kbd "s-[") . perspective-exwm-cycle-exwm-buffers-backward)
+	(,(kbd "s-]") . perspective-exwm-cycle-exwm-buffers-forward)
+	(,(kbd "s-<mouse-4>") . perspective-exwm-cycle-exwm-buffers-backward)
+	(,(kbd "s-<mouse-5>") . perspective-exwm-cycle-exwm-buffers-forward)
+	(,(kbd "s-`") . perspective-exwm-switch-perspective)
+	(,(kbd "s-o") . ,(my/app-command "rofi -show window"))
+
+	;; 's-N': Switch to certain workspace with Super (Win) plus a number key (0 - 9)
+	,@(mapcar (lambda (i)
+		    `(,(kbd (format "s-%d" i)) .
+		      (lambda ()
+			(interactive)
+			(when (or (< ,i (exwm-workspace--count))
+				  (y-or-n-p (format "Create workspace %d" ,i)))
+			  (exwm-workspace-switch-create ,i) ))))
+		  (number-sequence 0 9))))
+

A function to apply changes to exwm-input-global-keys.

+
(defun my/exwm-update-global-keys ()
+  (interactive)
+  (setq exwm-input--global-keys nil)
+  (dolist (i exwm-input-global-keys)
+    (exwm-input--set-key (car i) (cdr i)))
+  (when exwm--connection
+    (exwm-input--update-global-prefix-keys)))
+

App shortcuts

+

A transient hydra for shortcuts for the most frequent apps.

+
(defun my/run-in-background (command)
+  (let ((command-parts (split-string command "[ ]+")))
+    (apply #'call-process `(,(car command-parts) nil 0 nil ,@(cdr command-parts)))))
+
+(defhydra my/exwm-apps-hydra (:color blue :hint nil)
+  "
+^Apps^
+_t_: Terminal (Alacritty)
+_b_: Browser (Firefox)
+_s_: Rocket.Chat
+_e_: Telegram
+_d_: Discord
+"
+  ("t" (lambda () (interactive) (my/run-in-background "alacritty")))
+  ("b" (lambda () (interactive) (my/run-in-background "firefox")))
+  ("s" (lambda () (interactive) (my/run-in-background "flatpak run chat.rocket.RocketChat")))
+  ("e" (lambda () (interactive) (my/run-in-background "telegram-desktop")))
+  ("d" (lambda () (interactive) (my/run-in-background "flatpak run com.discordapp.Discord"))))
+

Locking up

+

Run i3lock.

+
(defun my/exwm-lock ()
+  (interactive)
+  (my/run-in-background "i3lock -f -i /home/pavel/Pictures/lock-wallpaper.png"))
+

Fixes

+

Catch and report all errors raised when invoking command hooks

+ + +
(defun exwm-input--fake-last-command ()
+  "Fool some packages into thinking there is a change in the buffer."
+  (setq last-command #'exwm-input--noop)
+  (condition-case hook-error
+      (progn
+	(run-hooks 'pre-command-hook)
+	(run-hooks 'post-command-hook))
+    ((error)
+     (exwm--log "Error occurred while running command hooks: %s\n\nBacktrace:\n\n%s"
+		hook-error
+		(with-temp-buffer
+		  (setq-local standard-output (current-buffer))
+		  (backtrace)
+		  (buffer-string))))))
+

Improve floating windows behavior

+

These 3 settings seem to cause particular trouble with floating windows. Setting them to nil improves the stability greatly.

+
(defun my/fix-exwm-floating-windows ()
+  (setq-local exwm-workspace-warp-cursor nil)
+  (setq-local mouse-autoselect-window nil)
+  (setq-local focus-follows-mouse nil))
+
+(add-hook 'exwm-floating-setup-hook #'my/fix-exwm-floating-windows)
+

Fix exwm–on-ClientMessage

+

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.

+
(defun exwm--on-ClientMessage-old (raw-data _synthetic)
+  "Handle ClientMessage event."
+  (let ((obj (make-instance 'xcb:ClientMessage))
+	type id data)
+    (xcb:unmarshal obj raw-data)
+    (setq type (slot-value obj 'type)
+	  id (slot-value obj 'window)
+	  data (slot-value (slot-value obj 'data) 'data32))
+    (exwm--log "atom=%s(%s)" (x-get-atom-name type exwm-workspace--current)
+	       type)
+    (cond
+     ;; _NET_NUMBER_OF_DESKTOPS.
+     ((= type xcb:Atom:_NET_NUMBER_OF_DESKTOPS)
+      (let ((current (exwm-workspace--count))
+	    (requested (elt data 0)))
+	;; Only allow increasing/decreasing the workspace number by 1.
+	(cond
+	 ((< current requested)
+	  (make-frame))
+	 ((and (> current requested)
+	       (> current 1))
+	  (let ((frame (car (last exwm-workspace--list))))
+	    (exwm-workspace--get-remove-frame-next-workspace frame)
+	    (delete-frame frame))))))
+     ;; _NET_CURRENT_DESKTOP.
+     ((= type xcb:Atom:_NET_CURRENT_DESKTOP)
+      (exwm-workspace-switch (elt data 0)))
+     ;; _NET_ACTIVE_WINDOW.
+     ((= type xcb:Atom:_NET_ACTIVE_WINDOW)
+      (let ((buffer (exwm--id->buffer id))
+	    iconic window)
+	(when (buffer-live-p buffer)
+	  (with-current-buffer buffer
+	    (when (eq exwm--frame exwm-workspace--current)
+	      (if exwm--floating-frame
+		  (select-frame exwm--floating-frame)
+		(setq iconic (exwm-layout--iconic-state-p))
+		(when iconic
+		  ;; State change: iconic => normal.
+		  (set-window-buffer (frame-selected-window exwm--frame)
+				     (current-buffer)))
+		;; Focus transfer.
+		(setq window (get-buffer-window nil t))
+		(when (or iconic
+			  (not (eq window (selected-window))))
+		  (select-window window))))))))
+     ;; _NET_CLOSE_WINDOW.
+     ((= type xcb:Atom:_NET_CLOSE_WINDOW)
+      (let ((buffer (exwm--id->buffer id)))
+	(when (buffer-live-p buffer)
+	  (exwm--defer 0 #'kill-buffer buffer))))
+     ;; _NET_WM_MOVERESIZE
+     ((= type xcb:Atom:_NET_WM_MOVERESIZE)
+      (let ((direction (elt data 2))
+	    (buffer (exwm--id->buffer id)))
+	(unless (and buffer
+		     (not (buffer-local-value 'exwm--floating-frame buffer)))
+	  (cond ((= direction
+		    xcb:ewmh:_NET_WM_MOVERESIZE_SIZE_KEYBOARD)
+		 ;; FIXME
+		 )
+		((= direction
+		    xcb:ewmh:_NET_WM_MOVERESIZE_MOVE_KEYBOARD)
+		 ;; FIXME
+		 )
+		((= direction xcb:ewmh:_NET_WM_MOVERESIZE_CANCEL)
+		 (exwm-floating--stop-moveresize))
+		;; In case it's a workspace frame.
+		((and (not buffer)
+		      (catch 'break
+			(dolist (f exwm-workspace--list)
+			  (when (or (eq id (frame-parameter f 'exwm-outer-id))
+				    (eq id (frame-parameter f 'exwm-id)))
+			    (throw 'break t)))
+			nil)))
+		(t
+		 ;; In case it's a floating frame,
+		 ;; move the corresponding X window instead.
+		 (unless buffer
+		   (catch 'break
+		     (dolist (pair exwm--id-buffer-alist)
+		       (with-current-buffer (cdr pair)
+			 (when
+			     (and exwm--floating-frame
+				  (or (eq id
+					  (frame-parameter exwm--floating-frame
+							   'exwm-outer-id))
+				      (eq id
+					  (frame-parameter exwm--floating-frame
+							   'exwm-id))))
+			   (setq id exwm--id)
+			   (throw 'break nil))))))
+		 ;; Start to move it.
+		 (exwm-floating--start-moveresize id direction))))))
+     ;; _NET_REQUEST_FRAME_EXTENTS
+     ((= type xcb:Atom:_NET_REQUEST_FRAME_EXTENTS)
+      (let ((buffer (exwm--id->buffer id))
+	    top btm)
+	(if (or (not buffer)
+		(not (buffer-local-value 'exwm--floating-frame buffer)))
+	    (setq top 0
+		  btm 0)
+	  (setq top (window-header-line-height)
+		btm (window-mode-line-height)))
+	(xcb:+request exwm--connection
+	    (make-instance 'xcb:ewmh:set-_NET_FRAME_EXTENTS
+			   :window id
+			   :left 0
+			   :right 0
+			   :top top
+			   :bottom btm)))
+      (xcb:flush exwm--connection))
+     ;; _NET_WM_DESKTOP.
+     ((= type xcb:Atom:_NET_WM_DESKTOP)
+      (let ((buffer (exwm--id->buffer id)))
+	(when (buffer-live-p buffer)
+	  (exwm-workspace-move-window (elt data 0) id))))
+     ;; _NET_WM_STATE
+     ((= type xcb:Atom:_NET_WM_STATE)
+      (let ((action (elt data 0))
+	    (props (list (elt data 1) (elt data 2)))
+	    (buffer (exwm--id->buffer id))
+	    props-new)
+	;; only support _NET_WM_STATE_FULLSCREEN / _NET_WM_STATE_ADD for frames
+	(when (and (not buffer)
+		   (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN props)
+		   (= action xcb:ewmh:_NET_WM_STATE_ADD))
+	  (xcb:+request
+	      exwm--connection
+	      (make-instance 'xcb:ewmh:set-_NET_WM_STATE
+			     :window id
+			     :data (vector xcb:Atom:_NET_WM_STATE_FULLSCREEN)))
+	  (xcb:flush exwm--connection))
+	(when buffer                    ;ensure it's managed
+	  (with-current-buffer buffer
+	    ;; _NET_WM_STATE_FULLSCREEN
+	    (when (or (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN props)
+		      (memq xcb:Atom:_NET_WM_STATE_ABOVE props))
+	      (cond ((= action xcb:ewmh:_NET_WM_STATE_ADD)
+		     (unless (exwm-layout--fullscreen-p)
+		       (exwm-layout-set-fullscreen id))
+		     (push xcb:Atom:_NET_WM_STATE_FULLSCREEN props-new))
+		    ((= action xcb:ewmh:_NET_WM_STATE_REMOVE)
+		     (when (exwm-layout--fullscreen-p)
+		       (exwm-layout-unset-fullscreen id)))
+		    ((= action xcb:ewmh:_NET_WM_STATE_TOGGLE)
+		     (if (exwm-layout--fullscreen-p)
+			 (exwm-layout-unset-fullscreen id)
+		       (exwm-layout-set-fullscreen id)
+		       (push xcb:Atom:_NET_WM_STATE_FULLSCREEN props-new)))))
+	    ;; _NET_WM_STATE_DEMANDS_ATTENTION
+	    ;; FIXME: check (may require other properties set)
+	    (when (memq xcb:Atom:_NET_WM_STATE_DEMANDS_ATTENTION props)
+	      (when (= action xcb:ewmh:_NET_WM_STATE_ADD)
+		(unless (eq exwm--frame exwm-workspace--current)
+		  (set-frame-parameter exwm--frame 'exwm-urgency t)
+		  (setq exwm-workspace--switch-history-outdated t)))
+	      ;; xcb:ewmh:_NET_WM_STATE_REMOVE?
+	      ;; xcb:ewmh:_NET_WM_STATE_TOGGLE?
+	      )
+	    (xcb:+request exwm--connection
+		(make-instance 'xcb:ewmh:set-_NET_WM_STATE
+			       :window id :data (vconcat props-new)))
+	    (xcb:flush exwm--connection)))))
+     ((= type xcb:Atom:WM_PROTOCOLS)
+      (let ((type (elt data 0)))
+	(cond ((= type xcb:Atom:_NET_WM_PING)
+	       (setq exwm-manage--ping-lock nil))
+	      (t (exwm--log "Unhandled WM_PROTOCOLS of type: %d" type)))))
+     ((= type xcb:Atom:WM_CHANGE_STATE)
+      (let ((buffer (exwm--id->buffer id)))
+	(when (and (buffer-live-p buffer)
+		   (= (elt data 0) xcb:icccm:WM_STATE:IconicState))
+	  (with-current-buffer buffer
+	    (if exwm--floating-frame
+		(call-interactively #'exwm-floating-hide)
+	      (bury-buffer))))))
+     (t
+      (exwm--log "Unhandled: %s(%d)"
+		 (x-get-atom-name type exwm-workspace--current) type)))))
+
+(with-eval-after-load 'exwm
+  (advice-add 'exwm--on-ClientMessage :override #'exwm--on-ClientMessage-old))
+

Application-specific settings

+

Start Nyxt in char-mode.

+
(setq exwm-manage-configurations
+   '(((member exwm-class-name '("Nyxt"))
+	   char-mode t)))
+

EXWM config

+

And the EXWM config itself.

+
(defun my/exwm-init ()
+  (exwm-workspace-switch 1)
+
+  (my/exwm-run-polybar)
+  (my/exwm-set-wallpaper)
+  (my/exwm-run-shepherd)
+  (my/run-in-background "gpgconf --reload gpg-agent"))
+
+(defun my/exwm-update-class ()
+  (exwm-workspace-rename-buffer (format "EXWM :: %s" exwm-class-name)))
+
+(defun my/exwm-set-alpha (alpha)
+  (setf (alist-get 'alpha default-frame-alist)
+	`(,alpha . ,alpha))
+  (cl-loop for frame being the frames
+	   do (set-frame-parameter frame 'alpha `(,alpha . ,alpha))))
+
+(use-package exwm
+  :straight t
+  :config
+  (setq exwm-workspace-number 5)
+  (add-hook 'exwm-init-hook #'my/exwm-init)
+  (add-hook 'exwm-update-class-hook #'my/exwm-update-class)
+
+  (require 'exwm-randr)
+  (exwm-randr-enable)
+  (start-process-shell-command "xrandr" nil "~/bin/scripts/screen-layout")
+  (when (string= (system-name) "indigo")
+    (setq my/exwm-another-monitor "DVI-D-0")
+    (setq exwm-randr-workspace-monitor-plist `(2 ,my/exwm-another-monitor 3 ,my/exwm-another-monitor)))
+
+  (setq exwm-workspace-warp-cursor t)
+  (setq mouse-autoselect-window t)
+  (setq focus-follows-mouse t)
+
+  <<exwm-workspace-config>>
+  <<exwm-keybindings>>
+  <<exwm-mode-line-config>>
+  <<exwm-fixes>>
+
+  (if (my/light-p)
+      (my/exwm-set-alpha 100)
+    (my/exwm-set-alpha 90))
+
+  (perspective-exwm-mode)
+  (exwm-enable))
+

i3wm

+ + + + + + + + + + + + + + + + + +
Guix dependencyDisabled
i3-gaps
i3locktrue
+

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.

+

References:

+ +

General settings

+
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.

+

Kill focused windows

+
bindsym $mod+Shift+q exec emacs-i3-integration kill
+

Change focus

+
bindsym $mod+h exec emacs-i3-integration focus left
+bindsym $mod+j exec emacs-i3-integration focus down
+bindsym $mod+k exec emacs-i3-integration focus up
+bindsym $mod+l exec emacs-i3-integration focus right
+
+bindsym $mod+Left exec emacs-i3-integration focus left
+bindsym $mod+Down exec emacs-i3-integration focus down
+bindsym $mod+Up exec emacs-i3-integration focus up
+bindsym $mod+Right exec emacs-i3-integration focus right
+

Move windows around

+
bindsym $mod+Shift+h exec emacs-i3-integration move left
+bindsym $mod+Shift+j exec emacs-i3-integration move down
+bindsym $mod+Shift+k exec emacs-i3-integration move up
+bindsym $mod+Shift+l exec emacs-i3-integration move right
+
+bindsym $mod+Shift+Left exec emacs-i3-integration move left
+bindsym $mod+Shift+Down exec emacs-i3-integration move down
+bindsym $mod+Shift+Up exec emacs-i3-integration move up
+bindsym $mod+Shift+Right exec emacs-i3-integration move right
+

Split windows

+
bindsym $mod+s exec emacs-i3-integration split h
+bindsym $mod+v exec emacs-i3-integration split v
+

Switch tabs

+
bindsym $mod+period exec i3-switch-tabs right
+bindsym $mod+comma exec i3-switch-tabs left
+

Enter fullscreen mode

+
# enter fullscreen mode for the focused container
+bindsym $mod+f fullscreen toggle
+bindsym $mod+c fullscreen toggle global
+

Changing layout

+
bindsym $mod+w layout stacking
+bindsym $mod+t layout tabbed
+bindsym $mod+e exec emacs-i3-integration layout toggle split
+

Toggle tiling/floating, switch between tiled and floating windows

+
bindsym $mod+Shift+f floating toggle
+bindsym $mod+z focus mode_toggle
+

Switching outputs

+
bindsym $mod+Tab move workspace to output right
+bindsym $mod+q focus output right
+

Focus parent and child container

+
bindsym $mod+a focus parent
+bindsym $mod+Shift+A focus child
+

Toggle sticky

+
bindsym $mod+Shift+i sticky toggle
+

Set windows as floating and sticky, move to the top right.

+
bindsym $mod+x floating enable; sticky enable; move position 1220 0; resize set width 700 px
+

Workspaces

+
set $w1 "1 🚀"
+set $w2 "2 🌍"
+set $w3 "3 💬"
+set $w4 "4 🛠️️"
+set $w7 "7 🛰️"
+set $w8 "8 📝"
+set $w9 "9 🎵"
+set $w10 "10 📦"
+
+bindsym $mod+1 workspace $w1
+bindsym $mod+2 workspace $w2
+bindsym $mod+3 workspace $w3
+bindsym $mod+4 workspace $w4
+bindsym $mod+5 workspace 5
+bindsym $mod+6 workspace 6
+bindsym $mod+7 workspace $w7
+bindsym $mod+8 workspace $w8
+bindsym $mod+9 workspace $w9
+bindsym $mod+0 workspace $w10
+
+# move focused container to workspace
+bindsym $mod+Shift+1 move container to workspace $w1
+bindsym $mod+Shift+2 move container to workspace $w2
+bindsym $mod+Shift+3 move container to workspace $w3
+bindsym $mod+Shift+4 move container to workspace $w4
+bindsym $mod+Shift+5 move container to workspace 5
+bindsym $mod+Shift+6 move container to workspace 6
+bindsym $mod+Shift+7 move container to workspace $w7
+bindsym $mod+Shift+8 move container to workspace $w8
+bindsym $mod+Shift+9 move container to workspace $w9
+bindsym $mod+Shift+0 move container to workspace $w10
+

Rules

+

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”.

+
assign [class="Emacs"] $w1
+assign [class="qutebrowser"] $w2
+assign [class="firefox"] $w2
+assign [class="VK"] $w3
+assign [class="Slack"] $w3
+assign [class="discord"] $w3
+assign [class="TelegramDesktop"] $w3
+assign [class="Postman"] $w4
+assign [class="Chromium-browse"] $w4
+assign [class="chromium"] $w4
+assign [class="google-chrome"] $w4
+assign [title="Vue Developer Tools"] $w4
+assign [class="Google Play Music Desktop Player"] $w9
+assign [class="jetbrains-datagrip"] $w4
+assign [class="zoom"] $w7
+assign [class="skype"] $w7
+assign [class="Mailspring"] $w8
+assign [class="Thunderbird"] $w8
+assign [class="Joplin"] $w8
+assign [class="keepassxc"] $w10
+
+for_window [title="VirtScreen"] floating enable
+
+for_window [title="ncmpcpp.*"] move to workspace $w9
+for_window [title="newsboat.*"] move to workspace $w9
+for_window [title=".*run_wego"] move to workspace $w9
+for_window [class="cinnamon-settings*"] floating enable
+for_window [title="Picture-in-Picture"] sticky enable
+for_window [window_role="GtkFileChooserDialog"] resize set width 1000 px height 800 px
+for_window [window_role="GtkFileChooserDialog"] move position center
+

Scratchpad

+

Scratch terminal, inspired by this Luke Smith’s video.

+

Launch script

+

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.

+
CLASSNAME="dropdown_tmux"
+COMMAND="alacritty --class $CLASSNAME -e tmux new-session -s $CLASSNAME"
+pid=$(xdotool search --classname "dropdown_tmux")
+if [[ ! -z $pid  ]]; then
+    i3-msg scratchpad show
+else
+    setsid -f ${COMMAND}
+fi
+

i3 config

+
# Scratchpad
+for_window [instance="dropdown_*"] floating enable
+for_window [instance="dropdown_*"] move scratchpad
+for_window [instance="dropdown_*"] sticky enable
+for_window [instance="dropdown_*"] scratchpad show
+for_window [instance="dropdown_*"] move position center
+
+bindsym $mod+u exec ~/bin/scripts/dropdown
+

Gaps & borders

+

The main reason to use i3-gaps

+
# Borders
+# for_window [class=".*"] border pixel 0
+default_border pixel 3
+hide_edge_borders both
+
+# Gaps
+set $default_inner 10
+set $default_outer 0
+
+gaps inner $default_inner
+gaps outer $default_outer
+
+smart_gaps on
+

Keybindings

+
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 set 0
+    bindsym Shift+0 gaps inner all set 0
+
+    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 set 0
+    bindsym Shift+0 gaps outer all set 0
+
+    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.

+
if [[ $(xdotool getactivewindow getwindowname) =~ ^emacs(:.*)?@.* ]]; then
+    emacsclient -e "(balance-windows)" &
+fi
+i3_balance_workspace
+
mode "resize" {
+
+    bindsym h exec emacs-i3-integration resize shrink width 10 px or 10 ppt
+    bindsym j exec emacs-i3-integration resize grow height 10 px or 10 ppt
+    bindsym k exec emacs-i3-integration resize shrink height 10 px or 10 ppt
+    bindsym l exec emacs-i3-integration resize grow width 10 px or 10 ppt
+
+    bindsym Shift+h exec emacs-i3-integration resize shrink width 100 px or 100 ppt
+    bindsym Shift+j exec emacs-i3-integration resize grow height 100 px or 100 ppt
+    bindsym Shift+k exec emacs-i3-integration resize shrink height 100 px or 100 ppt
+    bindsym Shift+l exec emacs-i3-integration resize grow width 100 px or 100 ppt
+
+    # same bindings, but for the arrow keys
+    bindsym Left  exec emacs-i3-integration resize shrink width 10 px or 10 ppt
+    bindsym Down  exec emacs-i3-integration resize grow height 10 px or 10 ppt
+    bindsym Up    exec emacs-i3-integration resize shrink height 10 px or 10 ppt
+    bindsym Right exec emacs-i3-integration resize grow width 10 px or 10 ppt
+
+    bindsym Shift+Left  exec emacs-i3-integration resize shrink width 100 px or 100 ppt
+    bindsym Shift+Down  exec emacs-i3-integration resize grow height 100 px or 100 ppt
+    bindsym Shift+Up    exec emacs-i3-integration resize shrink height 100 px or 100 ppt
+    bindsym Shift+Right exec emacs-i3-integration resize grow width 100 px or 100 ppt
+
+    bindsym equal exec i3-emacs-balance-windows
+
+    # back to normal: Enter or Escape
+    bindsym Return mode "default"
+    bindsym Escape mode "default"
+}
+
+bindsym $mod+r mode "resize"
+
+mode "move" {
+    bindsym $mod+Tab focus right
+
+    bindsym Left  move left
+    bindsym Down  move down
+    bindsym Up    move up
+    bindsym Right move right
+
+    bindsym h     move left
+    bindsym j     move down
+    bindsym k     move up
+    bindsym l     move right
+
+    # back to normal: Enter or Escape
+    bindsym Return mode "default"
+    bindsym Escape mode "default"
+}
+
+bindsym $mod+m mode "move" focus floating
+

OFF (OFF) Intergration with dmenu

+

dmenu is a dynamic menu program for X. I’ve opted out of using it in favour of rofi, but here is a relevant bit of config.

+

Scripts are located in the bin/scripts folder.

+
# dmenu
+bindsym $mod+d exec i3-dmenu-desktop --dmenu="dmenu -l 10"
+bindsym $mod+apostrophe mode "dmenu"
+
+mode "dmenu" {
+    bindsym d exec i3-dmenu-desktop --dmenu="dmenu -l 10"; mode default
+    bindsym p exec dmenu_run -l 10; mode default
+    bindsym m exec dmenu-man; mode default
+    bindsym b exec dmenu-buku; mode default
+    bindsym f exec dmenu-explore; mode default
+    bindsym t exec dmenu-tmuxp; mode default
+    bindsym Escape mode "default"
+}
+
+bindsym $mod+b exec --no-startup-id dmenu-buku
+

Integration with rofi

+

Keybindings to launch rofi. For more detail, look the Rofi section.

+
bindsym $mod+p exec "rofi -modi 'drun,run' -show drun"
+bindsym $mod+b exec --no-startup-id rofi-buku-mine
+bindsym $mod+minus exec rofi-pass
+bindsym $mod+equal exec rofimoji
+
+bindsym $mod+apostrophe mode "rofi"
+
+mode "rofi" {
+    bindsym d exec "rofi -modi 'drun,run' -show drun"
+    bindsym m exec rofi-man; mode default
+    bindsym b exec rofi-buku-mine; mode default
+    bindsym k exec rofi-pass; mode default
+    bindsym Escape mode "default"
+}
+

Launching apps & misc keybindings

+

I prefer to use a separate mode to launch most of my apps, with some exceptions.

+

Apps

+
# Launch apps
+# start a terminal at workspace 1
+bindsym $mod+Return exec "i3-msg 'workspace 1 🚀; exec alacritty'"
+
+bindsym $mod+i exec "copyq menu"
+bindsym $mod+Shift+x exec "i3lock -f -i /home/pavel/Pictures/lock-wallpaper.png"
+
+bindsym $mod+semicolon mode "apps"
+
+mode "apps" {
+    bindsym Escape mode "default"
+    bindsym b exec firefox; mode default
+    bindsym v exec vk; mode default
+    bindsym s exec slack-wrapper; mode default;
+    bindsym d exec "flatpak run com.discordapp.Discord"; mode default;
+    bindsym m exec "alacritty -e ncmpcpp"; mode default
+    bindsym c exec "copyq toggle"; mode default
+    bindsym k exec "keepassxc"; mode default
+    # bindsym e exec mailspring; mode default
+    bindsym a exec emacs; mode default
+    bindsym n exec "alacritty -e newsboat"; mode default
+    bindsym w exec "alacritty /home/pavel/bin/scripts/run_wego"; mode default
+    # bindsym a exec emacsclient -c; mode default
+    # bindsym Shift+a exec emacs; mode default
+}
+

Media controls & brightness

+
# Pulse Audio controls
+bindsym XF86AudioRaiseVolume exec --no-startup-id "ponymix increase 5 --max-volume 150"
+bindsym XF86AudioLowerVolume exec --no-startup-id "ponymix decrease 5 --max-volume 150"
+bindsym XF86AudioMute exec --no-startup-id "ponymix toggle"
+
+exec --no-startup-id xmodmap -e 'keycode 135 = Super_R' && xset -r 135
+bindsym $mod+F2 exec --no-startup-id "ponymix increase 5"
+bindsym $mod+F3 exec --no-startup-id "ponymix decrease 5"
+
+# Media player controls
+bindsym XF86AudioPlay exec mpc toggle
+bindsym XF86AudioPause exec mpc pause
+bindsym XF86AudioNext exec mpc next
+bindsym XF86AudioPrev exec mpc prev
+
+# Screen brightness
+bindsym XF86MonBrightnessUp exec light -A 5
+bindsym XF86MonBrightnessDown exec light -U 5
+

Screenshots

+
# Screenshots
+bindsym --release Print exec "flameshot gui"
+bindsym --release Shift+Print exec "xfce4-screenshooter"
+

Colors

+

Application of the XResources theme to the WM.

+
exec xrdb -merge $HOME/.Xresources
+
+# Colors
+set_from_resource $bg-color            background
+set_from_resource $active-color        color4
+set_from_resource $inactive-bg-color   color8
+set_from_resource $text-color          foreground
+set_from_resource $inactive-text-color color7
+set_from_resource $urgent-bg-color     color1
+set_from_resource $urgent-text-color   color0
+
+# window colors
+#                       border              background         text                 indicator       child border
+client.focused          $active-color       $bg-color          $text-color          $bg-color       $active-color
+client.unfocused        $bg-color           $inactive-bg-color $inactive-text-color $bg-color       $bg-color
+client.focused_inactive $active-color       $inactive-bg-color $inactive-text-color $bg-color       $bg-color
+client.urgent           $urgent-bg-color    $urgent-bg-color   $urgent-text-color   $bg-color       $urgent-bg-color
+

OFF (OFF) i3blocks

+

I’ve opted out of i3bar & i3blocks for polybar

+
bar {
+    status_command i3blocks -c ~/.config/i3/i3blocks.conf
+    i3bar_command i3bar
+    font pango:monospace 12
+    output HDMI-A-0
+    tray_output none
+    colors {
+	background $bg-color
+	separator #757575
+	#                  border             background         text
+	focused_workspace  $bg-color          $bg-color          $text-color
+	inactive_workspace $inactive-bg-color $inactive-bg-color $inactive-text-color
+	urgent_workspace   $urgent-bg-color   $urgent-bg-color   $urgent-text-color
+    }
+}
+
+bar {
+    status_command i3blocks -c ~/.config/i3/i3blocks.conf
+    i3bar_command i3bar
+    font pango:monospace 10
+    output DVI-D-0
+    colors {
+	background $bg-color
+	separator #757575
+	#                  border             background         text
+	focused_workspace  $bg-color          $bg-color          $text-color
+	inactive_workspace $inactive-bg-color $inactive-bg-color $inactive-text-color
+	urgent_workspace   $urgent-bg-color   $urgent-bg-color   $urgent-text-color
+    }
+}
+

Keyboard Layout

+

A script to set Russian-English keyboard layout:

+
setxkbmap -layout us,ru
+setxkbmap -model pc105 -option 'grp:win_space_toggle' -option 'grp:alt_shift_toggle'
+

A script to toggle the layout

+
if setxkbmap -query | grep -q us,ru; then
+    setxkbmap -layout us
+    setxkbmap -option
+else
+    setxkbmap -layout us,ru
+    setxkbmap -model pc105 -option 'grp:win_space_toggle' -option 'grp:alt_shift_toggle'
+fi
+

And the relevant i3 settings:

+
# Layout
+exec_always --no-startup-id set_layout
+bindsym $mod+slash exec toggle_layout
+

Autostart

+
# Polybar
+exec_always --no-startup-id "bash /home/pavel/bin/polybar.sh"
+
+# Wallpaper
+exec_always "feh --bg-fill ~/Pictures/wallpaper.jpg"
+
+# Picom
+exec picom
+
+# Keynav
+exec keynav
+
+# Applets
+exec --no-startup-id nm-applet
+# exec --no-startup-id /usr/bin/blueman-applet
+
+exec shepherd
+exec dunst
+exec copyq
+exec "xmodmap ~/.Xmodmap"
+# exec "xrdb -merge ~/.Xresources"
+# exec "bash ~/bin/autostart.sh"
+

Polybar

+ + + + + + + + + + + + + + + +
CategoryGuix dependencyDescription
desktop-polybarpolybarstatusbar
+

Polybar is a nice-looking, WM-agnostic statusbar program.

+

Don’t forget to install the Google Noto Color Emoji font. Guix package with all Noto fonts is way too large.

+

References:

+ +

General settings

+

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.

+

So…

+

+
(mapconcat
+ (lambda (elem)
+   (format "%s = %s" (car elem) (cdr elem)))
+ (append
+  (nreverse
+   (cl-reduce
+    (lambda (acc name)
+      (let* ((color (my/color-value name)))
+	(unless (member name '("black"))
+	  (setq color (ct-iterate
+		       color
+		       (lambda (c) (ct-edit-hsl-l-inc c 2))
+		       (lambda (c)
+			 (ct-light-p c 65)))))
+	(push (cons name color) acc)
+	(push (cons (format "light-%s" name)
+		    (ct-edit-lab-l-inc
+		     color
+		     my/alpha-for-light))
+	      acc))
+      acc)
+    '("black" "red" "green" "yellow" "blue" "magenta" "cyan" "white")
+    :initial-value nil))
+  `(("background" . ,(or (my/color-value 'bg-active)
+			 (my/color-value 'bg)))
+    ("foreground" . "#000000")))
+ "\n")
+
[colors]
+<<get-polybar-colors()>>
+

Glyph settings

+

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:

+ + + + + + + + + + + + + + + + + + + + + + + +
block1glyphblock2
foregroundF1B2F2
backgroundB1B1B2
+

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:

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IndexModuleColorGlyph
1pulseaudiolight-magenta+
2mpdmagenta+
9batterylight-cyan+
3cpucyan+
4ram-memorylight-green+
5swap-memorygreen+
6bandwidthlight-red+
7openvpnlight-red
8xkeyboardred+
10weatherlight-yellow+
12sunyellow+
13aw-afklight-blue+
14dateblue+
+

Also excluding some modules from certain monitors, which for now is about excluding battery from the monitors of my desktop PC:

+

+ + + + + + + + + + + + + + + + + +
MonitorExclude
DVI-D-0battery
HDMI-A-0battery
+

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:

+

+
(format
+ "${colors.%s}"
+ (nth
+  2
+  (seq-find
+   (lambda (el) (string-equal (nth 1 el) module))
+   table)))
+

That block is meant to be invoked in each module definition.

+

Generating glyphs

+

To generate the required set of glyphs, we need a glyph for every possible combination of adjacent colors that can occur in polybar.

+

Most of these combinations can be inferred from the polybar_modules table, the rest are defined in another table:

+

+ + + + + + + + + + + + + + + + + + + + + +
Color 1Color 2
backgroundwhite
backgroundlight-magenta
bluebackground
+

+
(let* ((monitors
+	(thread-last
+	  exclude-table
+	  (seq-map (lambda (el) (nth 0 el)))
+	  (seq-uniq)))
+       (exclude-combinations
+	(seq-map
+	 (lambda (monitor)
+	   (seq-map
+	    (lambda (el) (nth 1 el))
+	    (seq-filter
+	     (lambda (el) (and (string-equal (nth 0 el) monitor)
+			       (nth 1 el)))
+	     exclude-table)))
+	 `(,@monitors "")))
+       (module-glyph-combinations
+	(thread-last
+	  exclude-combinations
+	  (seq-map
+	   (lambda (exclude)
+	     (thread-last
+	       table
+	       (seq-filter
+		(lambda (elt)
+		  (not (or
+			(member (nth 1 elt) exclude)
+			(not (string-equal (nth 3 elt) "+")))))))))
+	  (seq-uniq)))
+       (color-changes nil))
+  (dolist (e extra)
+    (add-to-list
+     'color-changes
+     (concat (nth 0 e) "--" (nth 1 e))))
+  (dolist (comb module-glyph-combinations)
+    (dotimes (i (1- (length comb)))
+      (add-to-list
+       'color-changes
+       (concat (nth 2 (nth i comb))
+	       "--"
+	       (nth 2 (nth (1+ i) comb))))))
+  (mapconcat
+   (lambda (el)
+     (let ((colors (split-string el "--")))
+       (format "
+[module/glyph-%s--%s]
+type = custom/text
+content-background = ${colors.%s}
+content-foreground = ${colors.%s}
+content = ${glyph.gright}
+content-font = 5"
+	       (nth 0 colors)
+	       (nth 1 colors)
+	       (nth 0 colors)
+	       (nth 1 colors))))
+   color-changes
+   "\n"))
+

Here’s a rough outline of how the code works:

+
    +
  • monitors is a list of unique monitors in exclude-table
  • +
  • exclude-combilnations is a list of lists of module names to be excluded for each monitor
  • +
  • module-glyphs-combinations is a list of lists of actual modules for each monitor
  • +
  • color-changes is a list of unique adjacent colors across modules in all monitors
  • +
+

Finally, color-changes is used to generate glyph modules that look like this:

+
[module/glyph-light-cyan--cyan]
+type = custom/text
+content-background = ${colors.light-cyan}
+content-foreground = ${colors.cyan}
+content = ${glyph.gright}
+content-font = 5
+

As of now, 15 of such modules is generated.

+

Include this to the polybar config itself:

+
<<polybar-generate-glyphs()>>
+

Generating set of modules

+

To configure polybar itself, we need to generate a set of modules for each monitor.

+

The parameters here, excluding the two required tables, are:

+
    +
  • monitor - the current monitor on which to filter out the blocks by the polybar_modules_exclude table,
  • +
  • first-color - the first color of the first glyph,
  • +
  • last-color - the second color of the last glyph.
  • +
+ +

+
(let* ((exclude-modules
+	(thread-last
+	  exclude-table
+	  (seq-filter (lambda (el) (string-equal (nth 0 el) monitor)))
+	  (seq-map (lambda (el) (nth 1 el)))))
+       (modules
+	(thread-last
+	  table
+	  (seq-filter (lambda (el) (not (member (nth 1 el) exclude-modules))))))
+       (prev-color first-color)
+       (ret nil))
+  (concat
+   (mapconcat
+    (lambda (el)
+      (apply
+       #'concat
+       (list
+	(when (string-equal (nth 3 el) "+")
+	  (setq ret (format "glyph-%s--%s " prev-color (nth 2 el)))
+	  (setq prev-color (nth 2 el))
+	  ret)
+	(nth 1 el))))
+    modules
+    " ")
+   (unless (string-empty-p last-color) (format " glyph-%s--%s " prev-color last-color))))
+

The polybar config doesn’t support conditional statements, but it does support environment variables, so I pass the parameters from in the launch script.

+

Global bar config

+

Global bar configuration.

+

Monitor config and base colors.

+
[bar/mybar]
+monitor = ${env:MONITOR:}
+width = 100%
+height = ${env:HEIGHT:27}
+fixed-center = false
+bottom=true
+
+background = ${colors.background}
+foreground = ${colors.black}
+

Some geometry settings. These are set this way to make glyphs look the way they should

+
; line-size = 3
+line-color = #f00
+
+padding = 0
+
+module-margin-left = 0
+module-margin-right = 0
+margin-bottom = 0
+margin-top = 0
+
+; underline-size = 0
+border-size = 0
+
+offset-x = 0
+offset-y = 0
+radius = 0.0
+

Fonts

+
; font-0 = ${env:FONT0:pango:monospace:size=10;1}
+; font-1 = ${env:FONT1:NotoEmoji:scale=10:antialias=false;0}
+; font-2 = ${env:FONT2:fontawesome:pixelsize=10;1}
+; font-3 = ${env:FONT3:JetBrains Mono Nerd Font:monospace:size=10;1}
+
+font-0 = pango:monospace:size=13;2
+font-1 = NotoEmoji:scale=10:antialias=false;1
+font-2 = fontawesome:pixelsize=13;3
+font-3 = JetBrains Mono Nerd Font:monospace:size=13;4
+font-4 = JetBrains Mono Nerd Font:monospace:size=17;4
+

Modules. Because I sometimes set up different blocks on different monitors, they are set via environment variables.

+
modules-left = i3
+; modules-center = test
+modules-right = ${env:RIGHT_BLOCKS}
+
+tray-position = ${env:TRAY:right}
+tray-padding = 0
+tray-maxsize = 16
+tray-background = ${colors.background}
+
+wm-restack = i3
+; override-redirect = true
+
+scroll-up = i3wm-wsnext
+scroll-down = i3wm-wsprev
+
+; cursor-click = pointer
+; cursor-scroll = ns-resize
+

Misc settings.

+
[settings]
+screenchange-reload = true
+compositing-background = source
+compositing-foreground = over
+compositing-overline = over
+compositing-underline = over
+compositing-border = over
+
+[global/wm]
+margin-top = 0
+margin-bottom = 0
+

Launch script

+

The script below allows me to:

+
    +
  • 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
+export LOC="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
+    export MONITOR=$m
+    if [ "$MONITOR" = "$TRAY_MONITOR" ]; then
+	export TRAY="right"
+    else
+	export TRAY="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"
+    export HEIGHT=${BAR_HEIGHT[$MONITOR]}
+    export RIGHT_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.

+

pulseaudio

+

PulseAudio status

+
[module/pulseaudio]
+type = internal/pulseaudio
+use-ui-max = true
+
+bar-volume-width = 7
+; bar-volume-foreground-0 = ${colors.white}
+; bar-volume-foreground-1 = ${colors.yellow}
+; bar-volume-foreground-2 = ${colors.yellow}
+; bar-volume-foreground-3 = ${colors.blue}
+; bar-volume-foreground-4 = ${colors.blue}
+; bar-volume-foreground-5 = ${colors.green}
+; bar-volume-foreground-6 = ${colors.green}
+bar-volume-gradient = false
+bar-volume-indicator = |
+bar-volume-indicator-font = 2
+bar-volume-fill = 
+bar-volume-fill-font = 2
+bar-volume-empty = 
+bar-volume-empty-font = 2
+; bar-volume-empty-foreground = ${colors.light-white}
+
+format-volume = ♪ <ramp-volume> <label-volume>
+label-volume = %percentage%%
+
+ramp-volume-0 = 
+ramp-volume-1 = 
+ramp-volume-2 = 
+ramp-volume-3 = 
+ramp-volume-4 = 
+ramp-volume-5 = 
+ramp-volume-6 = 
+ramp-volume-7 = 
+
+format-muted = ♪ <label-muted>
+label-muted = MUTE
+
+format-volume-background = <<get-polybar-bg(module="pulseaudio")>>
+format-muted-background = <<get-polybar-bg(module="pulseaudio")>>
+format-volume-foreground = ${colors.foreground}
+format-muted-foreground = ${colors.foreground}
+
+; format-volume-underline = ${colors.white}
+; format-muted-underline = ${colors.light-black}
+

mpd

+

Music Player Daemon status

+
[module/mpd]
+type = internal/mpd
+
+format-playing = <toggle> <label-time> <label-song>
+format-paused = <toggle> <label-time> <label-song>
+format-stopped = " "
+label-song = [%album-artist%] %title%
+label-time = %elapsed%/%total%
+
+label-song-maxlen = 30
+label-song-ellipsis = true
+
+; format-playing-underline = ${colors.yellow}
+; format-paused-underline = ${colors.yellow}
+; format-stopped-underline = ${colors.yellow}
+
+format-playing-background = <<get-polybar-bg(module="mpd")>>
+format-paused-background = <<get-polybar-bg(module="mpd")>>
+format-stopped-background = <<get-polybar-bg(module="mpd")>>
+format-playing-foreground = ${colors.foreground}
+format-paused-foreground = ${colors.foreground}
+format-stopped-foreground = ${colors.foreground}
+
+label-separator = 0
+separator-foreground = ${colors.red}
+
+icon-pause = 
+icon-play = 
+icon-stop = 
+icon-prev = 1
+icon-next = 2
+

cpu

+

CPU usage

+
[module/cpu]
+type = internal/cpu
+format = " <label>"
+label = %percentage%%
+format-background = <<get-polybar-bg(module="cpu")>>
+format-foreground = ${colors.foreground}
+

ram-memory

+

RAM usage

+
[module/ram-memory]
+type = internal/memory
+interval = 10
+
+ramp-used-0 = 
+ramp-used-1 = 
+ramp-used-2 = 
+ramp-used-3 = 
+ramp-used-4 = 
+ramp-used-5 = 
+ramp-used-6 = 
+ramp-used-7 = 
+
+format =  <label>
+label=%gb_used:.1f%
+
+; format-underline = ${colors.blue}
+format-background = <<get-polybar-bg(module="ram-memory")>>
+format-foreground = ${colors.foreground}
+

swap-memory

+

Swap usage

+
[module/swap-memory]
+type = internal/memory
+interval = 10
+
+label= %gb_swap_used:.1f%
+format-background = <<get-polybar-bg(module="swap-memory")>>
+format-foreground = ${colors.foreground}
+

network

+

Upload/download speed

+

UPD <2022-07-24 Sun>: Somehow it doesn’t work with my current internet setup.

+
[module/network]
+type = internal/network
+interval = 1
+
+interface = ${env:WLAN_INTERFACE}
+
+; format-connected = [<ramp-signal>] <label-connected>
+
+label-connected = ↓ %downspeed% ↑ %upspeed%
+label-disconnected = X
+
+; format-connected-underline = ${colors.green}
+; format-disconnected-underline = ${colors.red}
+format-connected-background = <<get-polybar-bg(module="network")>>
+format-disconnected-background = <<get-polybar-bg(module="network")>>
+format-connected-foreground = ${colors.foreground}
+format-disconnected-foreground = ${colors.foreground}
+
+ramp-signal-0 = 0
+ramp-signal-1 = 1
+ramp-signal-2 = 2
+ramp-signal-3 = 3
+ramp-signal-4 = 4
+ramp-signal-5 = 5
+

bandwidth

+

My adaption of an i3blocks script called “bandwidth3”. I’ve only changed some defaults that are awkward to set with polybar.

+
[module/bandwidth]
+type = custom/script
+exec = /home/pavel/bin/polybar/bandwidth3.sh
+interval = 0
+tail = true
+
+format-background = <<get-polybar-bg(module="bandwidth")>>
+format-foreground = ${colors.foreground}
+
# Copyright (C) 2015 James Murphy
+# Copyright (C) 2022 Pavel Korytov
+# Licensed under the terms of the GNU GPL v2 only.
+
+iface="${BLOCK_INSTANCE}"
+iface="${IFACE:-$iface}"
+dt="${DT:-1}"
+unit="${UNIT:-KB}"
+printf_command="${PRINTF_COMMAND:-"printf \"↓ %-2.1f ↑ %2.1f [%s/s]\\n\", rx, wx, unit;"}"
+
+function default_interface {
+    ip route | awk '/^default via/ {print $5; exit}'
+}
+
+function check_proc_net_dev {
+    if [ ! -f "/proc/net/dev" ]; then
+	echo "/proc/net/dev not found"
+	exit 1
+    fi
+}
+
+function list_interfaces {
+    check_proc_net_dev
+    echo "Interfaces in /proc/net/dev:"
+    grep -o "^[^:]\\+:" /proc/net/dev | tr -d " :"
+}
+
+while getopts i:t:u:p:lh opt; do
+    case "$opt" in
+	i) iface="$OPTARG" ;;
+	t) dt="$OPTARG" ;;
+	u) unit="$OPTARG" ;;
+	p) printf_command="$OPTARG" ;;
+	l) list_interfaces && exit 0 ;;
+	h) printf \
+"Usage: bandwidth3 [-i interface] [-t time] [-u unit] [-p printf_command] [-l] [-h]
+Options:
+-i\tNetwork interface to measure. Default determined using \`ip route\`.
+-t\tTime interval in seconds between measurements. Default: 3
+-u\tUnits to measure bytes in. Default: Mb
+\tAllowed units: Kb, KB, Mb, MB, Gb, GB, Tb, TB
+\tUnits may have optional it/its/yte/ytes on the end, e.g. Mbits, KByte
+-p\tAwk command to be called after a measurement is made.
+\tDefault: printf \"<span font='FontAwesome'>  </span>%%-5.1f/%%5.1f %%s/s\\\\n\", rx, wx, unit;
+\tExposed variables: rx, wx, tx, unit, iface
+-l\tList available interfaces in /proc/net/dev
+-h\tShow this help text
+" && exit 0;;
+    esac
+done
+
+check_proc_net_dev
+
+iface="${iface:-$(default_interface)}"
+while [ -z "$iface" ]; do
+    echo No default interface
+    sleep "$dt"
+    iface=$(default_interface)
+done
+
+case "$unit" in
+    Kb|Kbit|Kbits)   bytes_per_unit=$((1024 / 8));;
+    KB|KByte|KBytes) bytes_per_unit=$((1024));;
+    Mb|Mbit|Mbits)   bytes_per_unit=$((1024 * 1024 / 8));;
+    MB|MByte|MBytes) bytes_per_unit=$((1024 * 1024));;
+    Gb|Gbit|Gbits)   bytes_per_unit=$((1024 * 1024 * 1024 / 8));;
+    GB|GByte|GBytes) bytes_per_unit=$((1024 * 1024 * 1024));;
+    Tb|Tbit|Tbits)   bytes_per_unit=$((1024 * 1024 * 1024 * 1024 / 8));;
+    TB|TByte|TBytes) bytes_per_unit=$((1024 * 1024 * 1024 * 1024));;
+    *) echo Bad unit "$unit" && exit 1;;
+esac
+
+scalar=$((bytes_per_unit * dt))
+init_line=$(cat /proc/net/dev | grep "^[ ]*$iface:")
+if [ -z "$init_line" ]; then
+    echo Interface not found in /proc/net/dev: "$iface"
+    exit 1
+fi
+
+init_received=$(awk '{print $2}' <<< $init_line)
+init_sent=$(awk '{print $10}' <<< $init_line)
+
+(while true; do cat /proc/net/dev; sleep "$dt"; done) |\
+    stdbuf -oL grep "^[ ]*$iface:"|\
+    awk -v scalar="$scalar" -v unit="$unit" -v iface="$iface" '
+BEGIN{old_received='"$init_received"';old_sent='"$init_sent"'}
+{
+    received=$2
+    sent=$10
+    rx=(received-old_received)/scalar;
+    wx=(sent-old_sent)/scalar;
+    tx=rx+wr;
+    old_received=received;
+    old_sent=sent;
+    if(rx >= 0 && wx >= 0){
+	'"$printf_command"';
+	fflush(stdout);
+    }
+}
+'
+

ipstack-vpn

+ + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryGuix dependencyDescription
desktop-polybarbind:utilsProvides dig
desktop-polybarcurl
desktop-polybarjqutil to work with JSONs
+

A module to get a country of the current IP and openvpn status. Uses ipstack API.

+
ip=$(dig +short +timeout=1 myip.opendns.com @resolver1.opendns.com 2> /dev/null)
+# API_KEY="$(pass show My_Online/APIs/ipstack | head -n 1)"
+API_KEY=$IPSTACK_API_KEY
+if [[ -z $ip || $ip == *"timed out"* ]]; then
+    echo "%{u<<get-color(name="red")>>}%{+u} ?? %{u-}"
+    exit
+fi
+ip_info=$(curl -s http://api.ipstack.com/${ip}?access_key=${API_KEY})
+# emoji=$(echo $ip_info | jq -r '.location.country_flag_emoji')
+code=$(echo $ip_info | jq -r '.country_code' 2> /dev/null)
+vpn=$(pgrep -a openvpn$ | head -n 1 | awk '{print $NF }' | cut -d '.' -f 1)
+
+if [[ -z $code ]]; then
+    code="??"
+fi
+
+if [ -n "$vpn" ]; then
+    echo "%{u<<get-color(name="blue")>>}%{+u}  $code %{u-}"
+else
+    echo "%{u<<get-color(name="red")>>}%{+u}  $code %{u-}"
+fi
+
[module/ipstack-vpn]
+type = custom/script
+exec = /home/pavel/bin/polybar/ipstack-vpn.sh
+interval = 1200
+

openvpn

+

A module to check if openvpn is running.

+
vpn=$(pgrep -a openvpn$ | head -n 1 | awk '{print $NF }' | cut -d '.' -f 1)
+if [ -n "$vpn" ]; then
+    echo "  "
+else
+    echo "  "
+fi
+
[module/openvpn]
+type = custom/script
+exec = /home/pavel/bin/polybar/openvpn.sh
+format-background = <<get-polybar-bg(module="openvpn")>>
+format-foreground = ${colors.foreground}
+interval = 1200
+

xkeyboard

+

Current keyboard layout

+
[module/xkeyboard]
+type = internal/xkeyboard
+format = <label-layout>
+
+; format-underline = ${colors.magenta}
+format-background = <<get-polybar-bg(module="xkeyboard")>>
+format-foreground = ${colors.foreground}
+label-layout = %icon%
+layout-icon-0 = ru;RU
+layout-icon-1 = us;US
+

battery

+
[module/battery]
+type = internal/battery
+battery = BAT0
+adapter = ADP0
+
+time-format = %H:%M
+format-discharging = <ramp-capacity> <label-discharging>
+format-discharging-background = <<get-polybar-bg(module="battery")>>
+format-charging-background = <<get-polybar-bg(module="battery")>>
+format-full-background = <<get-polybar-bg(module="battery")>>
+format-foreground = ${colors.foreground}
+label-discharging = %percentage%% %time%
+label-charging =  %percentage%% %time%
+
+ramp-capacity-0 = 
+ramp-capacity-1 = 
+ramp-capacity-2 = 
+ramp-capacity-3 = 
+ramp-capacity-4 = 
+

weather

+

Gets current weather from wttr.in

+
bar_format="${BAR_FORMAT:-"%t"}"
+location="${LOCATION:-"Saint-Petersburg"}"
+format_1=${FORMAT_1:-"qF"}
+format_2=${FORMAT_1:-"format=v2n"}
+
+bar_weather=$(curl -s wttr.in/${location}?format=${bar_format} || echo "??")
+if [ -z "$bar_weather" ]; then
+    exit 1
+elif [[ "$bar_weather" == *"Unknown"* || "$bar_weather" == *"Sorry"* || "$bar_weather" == *"Bad Gateway"* ]]; then
+    echo "??"
+    exit 1
+else
+    echo ${bar_weather}
+fi
+
[module/weather]
+type = custom/script
+exec = /home/pavel/bin/polybar/weather.sh
+; format-underline = ${colors.red}
+format-background = <<get-polybar-bg(module="weather")>>
+format-foreground = ${colors.foreground}
+interval = 1200
+

sun

+ + + + + + + + + + + + + +
CategoryGuix dependency
desktop-polybarsunwait
+

Prints out the time of sunrise/sunset. Uses sunwait

+
declare -A LAT_DATA=(
+    ["TMN"]="57.15N"
+    ["SPB"]="59.9375N"
+)
+declare -A LON_DATA=(
+    ["TMN"]="65.533333E"
+    ["SPB"]="30.308611E"
+)
+if [ -z "$LOC" ]; then
+    echo "LOC?"
+    exit -1
+fi
+LAT=${LAT_DATA[$LOC]}
+LON=${LON_DATA[$LOC]}
+
+time=$(sunwait poll daylight rise ${LAT} $LON)
+
+if [[ ${time} == 'DAY' ]]; then
+    sunset=$(sunwait list daylight set ${LAT} ${LON})
+    # echo "%{u<<get-color(name="yellow")>>}%{+u} $sunset %{u-}"
+    echo $sunset
+else
+    sunrise=$(sunwait list daylight rise ${LAT} ${LON})
+    # echo "%{u<<get-color(name="red")>>}%{+u} $sunrise %{u-}"
+    echo $sunrise
+fi
+
[module/sun]
+type = custom/script
+exec = /home/pavel/bin/polybar/sun.sh
+format-background = <<get-polybar-bg(module="sun")>>
+format-foreground = ${colors.foreground}
+interval = 60
+

aw-afk

+

Prints out a current uptime and non-AFK time from ActivityWatch server

+ + + + + + + + + + + + + +
CategoryGuix dependency
desktop-polybardateutils
+
afk_event=$(curl -s -X GET "http://localhost:5600/api/0/buckets/aw-watcher-afk_$(hostname)/events?limit=1" -H "accept: application/json")
+status=$(echo ${afk_event} | jq -r '.[0].data.status')
+afk_time=$(echo "${afk_event}" | jq -r '.[0].duration' | xargs -I !  date -u -d @! +"%H:%M")
+
+uptime=$(uptime | awk '{ print substr($3, 0, length($3) - 1) }' | xargs -I ! date -d ! +"%H:%M")
+res="${afk_time} / ${uptime}"
+if [[ $status == 'afk' ]]; then
+    # echo "%{u<<get-color(name="red")>>}%{+u} [AFK] $res %{u-}"
+    echo "[AFK] $res"
+else
+    # echo "%{u<<get-color(name="blue")>>}%{+u} $res %{u-}"
+    echo "$res"
+fi
+
[module/aw-afk]
+type = custom/script
+exec = /home/pavel/bin/polybar/aw_afk.sh
+interval = 60
+format-background = <<get-polybar-bg(module="aw-afk")>>
+format-foreground = ${colors.foreground}
+

date

+

Current date

+
[module/date]
+type = internal/date
+interval = 5
+
+date =
+date-alt = "%Y-%m-%d"
+
+time = %H:%M
+time-alt = %H:%M:%S
+
+format-background = <<get-polybar-bg(module="date")>>
+format-foreground = ${colors.foreground}
+label = "%date% %time%"
+

pomm

+

Pomodoro module.

+
if ps -e | grep emacs >> /dev/null; then
+    emacsclient --eval "(if (boundp 'pomm-current-mode-line-string) pomm-current-mode-line-string \"\") " | xargs echo -e
+fi
+
[module/pomm]
+type = custom/script
+exec = /home/pavel/bin/polybar/pomm.sh
+interval = 1
+format-underline = ${colors.light-green}
+format-foreground = ${colors.foreground}
+

SEP

+

A simple separator

+
[module/SEP]
+type = custom/text
+content = "|"
+content-foreground = ${colors.magenta}
+content-padding = 0
+content-margin = 0
+interval = 100000
+

TSEP

+

A separator, which appears only if monitor is set to have a tray in the launch script

+
if [ ! -z "$TRAY" ] && [ "$TRAY" != "none" ]; then
+    echo "| "
+fi
+
[module/TSEP]
+type = custom/script
+exec = /home/pavel/bin/polybar/tray-sep.sh
+format-foreground = ${colors.magenta}
+interval = 100000
+

i3

+

Show i3wm workspaces

+
[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

+ + + + + + + + + + + + + +
CategoryGuix dependency
desktop-rofirofi
+

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.

+

+
(apply
+ #'concat
+ (mapcar
+  (lambda (elem)
+    (concat (nth 0 elem) ": " (my/color-value (nth 0 elem)) ";\n"))
+  table))
+
/* Generated from [[file:../../Desktop.org::*Theme][Theme:1]] */
+ * {
+    <<get-rofi-colors()>>
+
+    foreground:                  <<get-color(name="fg")>>;
+    background:                  <<get-color(name="bg")>>;
+    background-color:            <<get-color(name="bg")>>;
+    separatorcolor:              @blue;
+    border-color:                <<get-color(name="border")>>;
+    selected-normal-background:  <<get-color(name="blue")>>;
+    selected-normal-foreground:  <<get-fg-for-color(name="blue")>>;
+    selected-active-background:  <<get-color(name="light-blue")>>;
+    selected-active-foreground:  <<get-fg-for-color(name="light-blue")>>;
+    selected-urgent-background:  <<get-color(name="red")>>;
+    selected-urgent-foreground:  <<get-fg-for-color(name="red")>>;
+    normal-foreground:           @foreground;
+    normal-background:           @background;
+    active-foreground:           @blue;
+    active-background:           @background;
+    urgent-foreground:           @red;
+    urgent-background:           @background;
+    alternate-normal-background: <<get-color(name="bg-alt")>>;
+    alternate-normal-foreground: @foreground;
+    alternate-active-background: <<get-fg-for-color(name="light-blue")>>;
+    alternate-active-foreground: <<get-color(name="light-blue")>>;
+    alternate-urgent-background: <<get-fg-for-color(name="red")>>;
+    alternate-urgent-foreground: <<get-color(name="red")>>;
+    spacing:                     2;
+}
+window {
+    background-color: @background;
+    border:           1;
+    padding:          5;
+}
+mainbox {
+    border:           0;
+    padding:          0;
+}
+message {
+    border:           1px dash 0px 0px ;
+    border-color:     @separatorcolor;
+    padding:          1px ;
+}
+textbox {
+    text-color:       @foreground;
+}
+listview {
+    fixed-height:     0;
+    border:           2px dash 0px 0px ;
+    border-color:     @separatorcolor;
+    spacing:          2px ;
+    scrollbar:        true;
+    padding:          2px 0px 0px ;
+}
+element {
+    border:           0;
+    padding:          1px ;
+}
+element normal.normal {
+    background-color: @normal-background;
+    text-color:       @normal-foreground;
+}
+element normal.urgent {
+    background-color: @urgent-background;
+    text-color:       @urgent-foreground;
+}
+element normal.active {
+    background-color: @active-background;
+    text-color:       @active-foreground;
+}
+element selected.normal {
+    background-color: @selected-normal-background;
+    text-color:       @selected-normal-foreground;
+}
+element selected.urgent {
+    background-color: @selected-urgent-background;
+    text-color:       @selected-urgent-foreground;
+}
+element selected.active {
+    background-color: @selected-active-background;
+    text-color:       @selected-active-foreground;
+}
+element alternate.normal {
+    background-color: @alternate-normal-background;
+    text-color:       @alternate-normal-foreground;
+}
+element alternate.urgent {
+    background-color: @alternate-urgent-background;
+    text-color:       @alternate-urgent-foreground;
+}
+element alternate.active {
+    background-color: @alternate-active-background;
+    text-color:       @alternate-active-foreground;
+}
+scrollbar {
+    width:            4px ;
+    border:           0;
+    handle-color:     @normal-foreground;
+    handle-width:     8px ;
+    padding:          0;
+}
+sidebar {
+    border:           2px dash 0px 0px ;
+    border-color:     @separatorcolor;
+}
+button {
+    spacing:          0;
+    text-color:       @normal-foreground;
+}
+button selected {
+    background-color: @selected-normal-background;
+    text-color:       @selected-normal-foreground;
+}
+inputbar {
+    spacing:          0px;
+    text-color:       @normal-foreground;
+    padding:          1px ;
+    children:         [ prompt,textbox-prompt-colon,entry,case-indicator ];
+}
+case-indicator {
+    spacing:          0;
+    text-color:       @normal-foreground;
+}
+entry {
+    spacing:          0;
+    text-color:       @normal-foreground;
+}
+prompt {
+    spacing:          0;
+    text-color:       @normal-foreground;
+}
+textbox-prompt-colon {
+    expand:           false;
+    str:              ":";
+    margin:           0px 0.3000em 0.0000em 0.0000em ;
+    text-color:       inherit;
+}
+

Scripts

+

Man pages

+

Inspired by this Luke Smith’s video.

+

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.

+
SELECTED=$(man -k . | rofi -dmenu -l 20 | awk '{print $1}')
+if [[ ! -z $SELECTED ]]; then
+    man -Tpdf $SELECTED | zathura -
+fi
+

Emojis

+ + + + + + + + + + + + + +
CategoryGuix dependency
desktop-rofipython-rofimoji
+

pass

+ + + + + + + + + + + + + + + + + +
CategoryGuix dependency
desktop-rofirofi-pass
desktop-rofixset
+

A nice pass frontend for Rofi, which is even packaged for Guix.

+
USERNAME_field='username'
+EDITOR=vim
+default_autotype='username :tab pass'
+clip=both
+

Flameshot

+ + + + + + + + + + + +
Guix dependency
flameshot
+

flameshot is my program of choice to make screenshots.

+

As it overwrites its own config all the time, I do not keep the file in VC.

+
[General]
+disabledTrayIcon=false
+drawColor=#ff0000
+drawThickness=3
+savePath=/home/pavel/Pictures
+savePathFixed=false
+showStartupLaunchMessage=false
+uiColor=<<get-color(name="blue")>>
+
+[Shortcuts]
+TYPE_ARROW=A
+TYPE_CIRCLE=C
+TYPE_CIRCLECOUNT=
+TYPE_COMMIT_CURRENT_TOOL=Ctrl+Return
+TYPE_COPY=Ctrl+C
+TYPE_DRAWER=D
+TYPE_EXIT=Ctrl+Q
+TYPE_IMAGEUPLOADER=Return
+TYPE_MARKER=M
+TYPE_MOVESELECTION=Ctrl+M
+TYPE_MOVE_DOWN=Down
+TYPE_MOVE_LEFT=Left
+TYPE_MOVE_RIGHT=Right
+TYPE_MOVE_UP=Up
+TYPE_OPEN_APP=Ctrl+O
+TYPE_PENCIL=P
+TYPE_PIN=
+TYPE_PIXELATE=B
+TYPE_RECTANGLE=R
+TYPE_REDO=Ctrl+Shift+Z
+TYPE_RESIZE_DOWN=Shift+Down
+TYPE_RESIZE_LEFT=Shift+Left
+TYPE_RESIZE_RIGHT=Shift+Right
+TYPE_RESIZE_UP=Shift+Up
+TYPE_SAVE=Ctrl+S
+TYPE_SELECTION=S
+TYPE_SELECTIONINDICATOR=
+TYPE_SELECT_ALL=Ctrl+A
+TYPE_TEXT=T
+TYPE_TOGGLE_PANEL=Space
+TYPE_UNDO=Ctrl+Z
+

dunst

+ + + + + + + + + + + + + + +
Guix dependency
dunst
libnotify
+

dunst is a lightweight notification daemon.

+

My customizations of the original config consist mostly of changing colors. Check out the default config or man dunst for the description of settings.

+

References:

+ + +
[global]
+    monitor = 0
+    follow = mouse
+    geometry = "300x5-30+20"
+    indicate_hidden = yes
+    shrink = no
+    transparency = 15
+    notification_height = 0
+    separator_height = 2
+    padding = 8
+    horizontal_padding = 8
+    frame_width = 1
+    frame_color = <<get-color(name="border", quote=1)>>
+    separator_color = frame
+    sort = yes
+    idle_threshold = 120
+
+    ### Text ###
+    font = DejaVu Sans 9
+
+    line_height = 0
+    markup = full
+
+    # The format of the message.  Possible variables are:
+    #   %a  appname
+    #   %s  summary
+    #   %b  body
+    #   %i  iconname (including its path)
+    #   %I  iconname (without its path)
+    #   %p  progress value if set ([  0%] to [100%]) or nothing
+    #   %n  progress value if set without any extra characters
+    #   %%  Literal %
+    # Markup is allowed
+    format = "<b>%s</b>\n%b"
+    alignment = left
+    show_age_threshold = 60
+    word_wrap = yes
+    ellipsize = middle
+    ignore_newline = no
+    stack_duplicates = true
+    hide_duplicate_count = false
+    show_indicators = yes
+
+    ### Icons ###
+    icon_position = left
+    max_icon_size = 32
+    icon_path = /usr/share/icons/Mint-Y/status/32/;/usr/share/icons/Mint-Y/devices/32
+
+    ### History ###
+    sticky_history = yes
+    history_length = 20
+
+    ### Misc/Advanced ###
+    dmenu = /usr/bin/dmenu -p dunst:
+    browser = /home/pavel/.guix-extra-profiles/browsers/browsers/bin/firefox
+    always_run_script = true
+    title = Dunst
+    class = Dunst
+    startup_notification = false
+    verbosity = mesg
+    corner_radius = 0
+
+    ### Legacy
+    force_xinerama = false
+
+    ### mouse
+    mouse_left_click = close_current
+    mouse_middle_click = do_action
+    mouse_right_click = close_all
+
+[experimental]
+    per_monitor_dpi = false
+
+[shortcuts]
+    close = ctrl+space
+    close_all = ctrl+shift+space
+    history = ctrl+grave
+    context = ctrl+shift+period
+
+[urgency_low]
+    background = <<get-color(name="bg-other", quote=1)>>
+    frame_color = <<get-color(name="border", quote=1)>>
+    foreground = <<get-color(name="fg", quote=1)>>
+    timeout = 10
+
+[urgency_normal]
+    background = <<get-color(name="bg", quote=1)>>
+    frame_color = <<get-color(name="border", quote=1)>>
+    foreground = <<get-color(name="fg", quote=1)>>
+    timeout = 10
+
+[urgency_critical]
+    background = <<get-color(name="red", quote=1)>>
+    foreground = <<get-fg-for-color(name="red", quote=1)>>
+    frame_color = <<get-color(name="red", quote=1)>>
+    timeout = 0
+

Firefox

+

Firefox is my web browser of choice.

+

Tridactyl

+

Tridactyl is a Firefox add-on that provides vim-like interface.

+

Run :nativeinstall at the first start.

+

Config

+

The native messenger allows to configure the addon with a config file.

+
sanitize tridactyllocal tridactylsync
+
+bind gn tabnew
+bind gN tabclose
+
+bind O fillcmdline tabopen
+
+bind n findnext 1
+bind N findnext -1
+bind F hint -t
+
+unbind <C-f>
+
+set smoothscroll false
+set findcase sensitive
+colorscheme emacs
+
+bind j scrollline 3
+bind k scrollline -3
+bind --mode=normal <C-i> mode ignore
+bind --mode=ignore <C-i> mode normal
+
+guiset_quiet gui full
+guiset_quiet statuspanel left
+guiset_quiet navbar none
+guiset_quiet tabs always
+
+set searchurls.g https://google.com/search?q=
+
+set newtab about:blank
+
+command fixamo_quiet jsb tri.excmds.setpref("privacy.resistFingerprinting.block_mozAddonManager", "true").then(tri.excmds.setpref("extensions.webextensions.restrictedDomains", '""'))
+command fixamo js tri.excmds.setpref("privacy.resistFingerprinting.block_mozAddonManager", "true").then(tri.excmds.setpref("extensions.webextensions.restrictedDomains", '""').then(tri.excmds.fillcmdline_tmp(3000, "Permissions added to user.js. Please restart Firefox to make them take affect.")))
+fixamo_quiet
+

Theme

+

Then, the package has its separate theme.

+

I based it on base16-dracula by Chris Kempson, but replaced the colors with my Emacs theme.

+
:root {
+  --tridactyl-fg: <<get-color(name="fg")>>;
+  --tridactyl-bg: <<get-color(name="bg")>>;
+  --tridactyl-url-fg: <<get-color(name="red")>>;
+  --tridactyl-url-bg: <<get-color(name="bg")>>;
+  --tridactyl-highlight-box-bg: <<get-color(name="blue")>>;
+  --tridactyl-highlight-box-fg: <<get-fg-for-color(name="blue")>>;
+
+  /* Command line */
+  --tridactyl-cmdl-bg: <<get-color(name="bg-alt")>>
+  --tridactyl-cmdl-fg: <<get-color(name="fg")>>
+
+  /* Hint character tags */
+  --tridactyl-hintspan-fg: <<get-fg-for-color(name="blue")>> !important;
+  --tridactyl-hintspan-bg: <<get-color(name="blue")>> !important;
+
+  /* Element Highlights */
+  --tridactyl-hint-active-fg: none;
+  --tridactyl-hint-active-bg: none;
+  --tridactyl-hint-active-outline: none;
+  /* --tridactyl-hint-activy-outline: var(--base08); */
+  --tridactyl-hint-bg: none;
+  --tridactyl-hint-outline: none;
+  /* --tridactyl-hint-outline: var(--base08); */
+}
+
+/* a { */
+/*   color: var(--base04); */
+/* } */
+
+#command-line-holder {
+  order: 1;
+  border: 2px solid <<get-color(name="blue")>>;
+  background: <<get-color(name="bg")>>;
+}
+
+#tridactyl-input {
+  padding: 1rem;
+  color: var(--tridactyl-fg);
+  width: 90%;
+  font-size: 1.2rem;
+  line-height: 1.5;
+  background: var(--tridactyl-bg);
+  padding-left: unset;
+  padding: 1rem;
+}
+
+#completions table {
+  font-size: 0.8rem;
+  font-weight: 200;
+  border-spacing: 0;
+  table-layout: fixed;
+  padding: 1rem;
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+
+#completions > div {
+  max-height: calc(20 * var(--option-height));
+  min-height: calc(10 * var(--option-height));
+}
+
+/* COMPLETIONS */
+
+#completions {
+  --option-height: 1.4em;
+  color: var(--tridactyl-fg);
+  background: var(--tridactyl-bg);
+  display: inline-block;
+  font-size: unset;
+  font-weight: 200;
+  overflow: hidden;
+  width: 100%;
+  border-top: unset;
+  order: 2;
+}
+
+/* Olie doesn't know how CSS inheritance works */
+#completions .HistoryCompletionSource {
+  max-height: unset;
+  min-height: unset;
+}
+
+#completions .HistoryCompletionSource table {
+  width: 100%;
+  font-size: 11pt;
+  border-spacing: 0;
+  table-layout: fixed;
+}
+
+/* redundancy 2: redundancy 2: more redundancy */
+#completions .BmarkCompletionSource {
+  max-height: unset;
+  min-height: unset;
+}
+
+#completions table tr td.prefix,#completions table tr td.privatewindow,#completions table tr td.container,#completions table tr td.icon {
+  display: none;
+}
+
+#completions .BufferCompletionSource table {
+  width: unset;
+  font-size: unset;
+  border-spacing: unset;
+  table-layout: unset;
+}
+
+#completions table tr .title {
+  width: 50%;
+}
+
+#completions table tr {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+#completions .sectionHeader {
+  background: unset;
+  font-weight: 200;
+  border-bottom: unset;
+  padding: 1rem !important;
+  padding-left: unset;
+  padding-bottom: 0.2rem;
+}
+
+#cmdline_iframe {
+  position: fixed !important;
+  bottom: unset;
+  top: 25% !important;
+  left: 10% !important;
+  z-index: 2147483647 !important;
+  width: 80% !important;
+  box-shadow: rgba(0, 0, 0, 0.5) 0px 0px 20px !important;
+}
+
+.TridactylStatusIndicator {
+  position: fixed !important;
+  bottom: 0 !important;
+  background: var(--tridactyl-bg) !important;
+  border: unset !important;
+  border: 1px <<get-color(name="blue")>> solid !important;
+  font-size: 12pt !important;
+  /*font-weight: 200 !important;*/
+  padding: 0.8ex !important;
+}
+
+#completions .focused {
+  background: <<get-color(name="blue")>>;
+  color: <<get-fg-for-color(name="blue")>>;
+}
+
+#completions .focused .url {
+  background: <<get-color(name="blue")>>;
+  color: <<get-fg-for-color(name="blue")>>;
+}
+/* #Ocean-normal { */
+/*  border-color: green !important; */
+/* } */
+
+/* #Ocean-insert { */
+/*  border-color: yellow !important; */
+/* } */
+

Firefox Color

+

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:

+
const JsonUrl = require('json-url');
+const jsonCodec = JsonUrl('lzma');
+
+const json = JSON.parse(process.argv[2]);
+jsonCodec.compress(json).then((r) => process.stdout.write(r));
+

Which I then can use to create the URL.

+
(defun my/firefox-encode-json (string)
+  (with-output-to-string
+    (with-current-buffer standard-output
+      (call-process "node" nil t nil
+		    (expand-file-name "~/bin/firefox-theme/main.js")
+		    string))))
+
+(defun my/color-value-rgb (color)
+  (let ((color (if (stringp color)
+		   color
+		 (my/color-value color))))
+    `((r . ,(* 2.55 (ct-get-rgb-r color)))
+      (g . ,(* 2.55 (ct-get-rgb-g color)))
+      (b . ,(* 2.55 (ct-get-rgb-b color))))))
+
+(defun my/firefox-get-json ()
+  (let ((toolbar-color
+	 (my/color-value-rgb
+	  (or
+	   (my/color-value 'bg-mode-line-active)
+	   (my/color-value 'bg-mode-line)
+	   (if (my/light-p)
+	       (ct-edit-hsl-l-dec (my/color-value 'bg-alt) 10)
+	     (ct-edit-hsl-l-inc (my/color-value 'bg-alt) 15)))))
+	(text-color
+	 (my/color-value-rgb
+	  (if (my/light-p) 'fg 'yellow))))
+    `((colors . ((toolbar . ,toolbar-color)
+		 (toolbar_text . ,text-color)
+		 (frame . ,(my/color-value-rgb 'bg))
+		 (tab_background_text . ,(my/color-value-rgb 'fg))
+		 (toolbar_field . ,(my/color-value-rgb 'bg))
+		 (toolbar_field_text . ,(my/color-value-rgb 'blue))
+		 (tab_line . ,text-color)
+		 (popup . ,(my/color-value-rgb 'bg-alt))
+		 (popup_text . ,(my/color-value-rgb 'fg))
+		 (tab_loading . ,text-color))))))
+
+(defun my/firefox-get-color-url ()
+  (concat
+   "https://color.firefox.com/?theme="
+   (my/firefox-encode-json
+    (json-encode
+     (my/firefox-get-json)))))
+
+(defun my/firefox-kill-color-url ()
+  (interactive)
+  (kill-new (my/firefox-get-color-url)))
+
+(my/firefox-get-color-url)
+

keynav

+ + + + + + + + + + + +
Guix dependency
keynav
+ + + + + + + + + + + + + +
TypeNote
SYMLINK./config/keynavrc -> .keynavrc
+

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…

+

References:

+ +

Config

+
# clear all previous keybindings
+clear
+
+# Start & stop
+ctrl+semicolon start
+Super_L+bracketright start
+Super_R+bracketright start
+Escape end
+ctrl+bracketleft end
+
+# Macros
+q record ~/.keynav_macros
+shift+at playback
+
+# Bisecting
+a history-back
+Left cut-left
+Right cut-right
+Down cut-down
+Up cut-up
+h cut-left
+j cut-down
+k cut-up
+l cut-right
+t windowzoom                          # Zoom to the current window
+c cursorzoom 300 300                  # Limit the bisection area by 300x300
+
+# Move the bisecting area
+shift+h move-left
+shift+j move-down
+shift+k move-up
+shift+l move-right
+shift+Left move-left
+shift+Right move-right
+shift+Up move-up
+shift+Down move-down
+
+# Actions
+space warp,click 3,end                # Right click
+Return warp,click 1,end               # Left click
+Shift+Return warp,doubleclick 1,end   # Double left click
+semicolon warp,end                    # Move the cursor and exit
+w warp                                # Just move the cursor
+e end                                 # exit
+u warp,click 4                        # scroll up
+d warp,click 5                        # scroll down
+1 click 1
+2 click 2
+3 click 3
+4 click 4
+5 click 5
+

Using with picom

+

I’ve noticed that the program does not play nice with picom’s fade effect. To fix that, add the following to you config:

+
fade-exclude = [
+  "class_i = 'keynav'",
+  "class_g = 'keynav'",
+]
+

Picom

+ + + + + + + + + + + +
Guix dependency
picom
+

picom is a compositor for X11. It allows effects such as transparency, blurring, etc.

+

Check out the sample configuration to get an idea on what’s possible. I only have some basic settings in mine.

+

Also, there are some fancy forks of picom (e.g. ibhagwan/picom adds rounded corners).

+

References:

+ +

Shadows

+
shadow = true;
+shadow-radius = 2;
+shadow-offset-x = -2;
+shadow-offset-y = -2;
+
+shadow-exclude = [
+  "name = 'Notification'",
+  "class_g = 'Conky'",
+  "name ?= 'cpt_frame_window'",
+  "class_g ?= 'Notify-osd'",
+  "class_g = 'Cairo-clock'",
+  "_GTK_FRAME_EXTENTS@:c"
+];
+

Fading

+
fading = true
+
+fade-in-step = 0.03;
+fade-out-step = 0.03;
+fade-delta = 10
+
+fade-exclude = [
+  "class_i = 'keynav'",
+  "class_g = 'keynav'",
+  "class_i = 'emacs'",
+  "class_g = 'emacs'",
+]
+

Opacity

+

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%.

+
inactive-opacity = 1;
+
+frame-opacity = 1.0;
+inactive-opacity-override = false;
+focus-exclude = [ "class_g = 'Cairo-clock'" ];
+
+opacity-rule = [
+  "0:_NET_WM_STATE@[0]:32a = '_NET_WM_STATE_HIDDEN'",
+  "0:_NET_WM_STATE@[1]:32a = '_NET_WM_STATE_HIDDEN'",
+  "0:_NET_WM_STATE@[2]:32a = '_NET_WM_STATE_HIDDEN'",
+  "0:_NET_WM_STATE@[3]:32a = '_NET_WM_STATE_HIDDEN'",
+  "0:_NET_WM_STATE@[4]:32a = '_NET_WM_STATE_HIDDEN'",
+  "90:class_g = 'Emacs'"
+];
+

General settings

+

Default general settings. Editing some of these may be neeeded in case of performance issues.

+
backend = "xrender";
+vsync = true
+mark-wmwin-focused = true;
+mark-ovredir-focused = true;
+detect-rounded-corners = true;
+detect-client-opacity = true;
+refresh-rate = 0
+detect-transient = true
+detect-client-leader = true
+use-damage = true
+log-level = "warn";
+
+wintypes:
+{
+  tooltip = { fade = true; shadow = true; opacity = 0.75; focus = true; full-shadow = false; };
+  dock = { shadow = false; }
+  dnd = { shadow = false; }
+  popup_menu = { opacity = 1; }
+  dropdown_menu = { opacity = 1; }
+};
+

Zathura

+ + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryGuix dependency
officezathura
officezathura-ps
officezathura-pdf-mupdf
officezathura-djvu
+

Zathura is a pdf viewer with vim-like keybindings.

+

+
(if (my/light-p) "false" "true")
+
set abort-clear-search false
+set guioptions cs
+set selection-clipboard clipboard
+set recolor <<zathura-recolor()>>
+map <C-r> set recolor false
+map <C-R> set recolor true
+
+set recolor-lightcolor <<get-color(name="black", quote=1)>>
+
+set completion-bg <<get-color(name="bg", quote=1)>>
+set completion-fg <<get-color(name="fg", quote=1)>>
+set completion-group-bg <<get-color(name="bg", quote=1)>>
+set completion-group-fg <<get-color(name="fg", quote=1)>>
+set completion-highlight-bg <<get-color(name="magenta", quote=1)>>
+set completion-highlight-fg <<get-fg-for-color(name="magenta", quote=1)>>
+
+set inputbar-bg <<get-color(name="light-black", quote=1)>>
+set inputbar-fg <<get-color(name="white", quote=1)>>
+set statusbar-bg <<get-color(name="light-black", quote=1)>>
+set statusbar-fg <<get-color(name="white", quote=1)>>
+
+set notification-error-bg <<get-color(name="red", quote=1)>>
+set notification-error-fg <<get-fg-for-color(name="red", quote=1)>>
+set notification-warning-bg <<get-color(name="yellow", quote=1)>>
+set notification-warning-fg <<get-fg-for-color(name="yellow", quote=1)>>
+

Various software

+

This section generates manifests for various desktop software that I’m using.

+

Browsers

+ + + + + + + + + + + + + + + + + +
CategoryGuix dependency
browsersungoogled-chromium
browsersfirefox
+

Office & Multimedia

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryGuix dependency
officelibreoffice
officegimp
officekrita
officeffmpeg
officekdenlive
officeinkscape
officeokular
officeobs
+

LaTeX

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryGuix dependency
latextexlive
latextexlab-bin
latexbiber
latexpython-pygments
latexfont-microsoft-web-core-fonts
+

Dev

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryGuix dependencyDisabled
devmicromamba-bin
devpandoc
devdocker-compose
devpostgresql
devvirt-manager
devgit-filter-repo
devnode
devopenjdk:jdk
devgo
devgopls
devpkg-config
devgcc-toolchain
devlua
devlibfaketime
devhugo-extended
devmake
devsbclt
devgit-lfs
devmysqlt
devgource
devphp
devpython
devpython-virtualenv
devleiningen
devsocat
devwireshark
+

Manifests

+

+
(my/format-guix-dependencies category)
+

Dev

+
(specifications->manifest
+ '(
+   <<packages("dev")>>))
+

Browsers

+
(specifications->manifest
+ '(
+   <<packages("browsers")>>))
+

Music

+
(specifications->manifest
+ '(
+   <<packages("music")>>))
+

Office

+
(specifications->manifest
+ '(
+   <<packages("office")>>))
+

LaTeX

+
(specifications->manifest
+ '(
+   <<packages("latex")>>))
+

Desktop Misc

+
(specifications->manifest
+ '(
+   <<packages("desktop-misc")>>))
+

Desktop polybar

+
(specifications->manifest
+ '(
+   <<packages("desktop-polybar")>>))
+

Desktop rofi

+
(specifications->manifest
+ '(
+   <<packages("desktop-rofi")>>))
+

Flatpak

+

A lot of proprietary desktop applications can be installed most easily with flatpak & flathub.

+ + + + + + + + + + + + + + +
Guix dependency
flatpak
xdg-desktop-portal
+

After installation, add the following repositories:

+
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
+flatpak remote-add --user --if-not-exists flathub-beta https://flathub.org/beta-repo/flathub-beta.flatpakrepo
+

Installation syntax is as follows:

+
flatpak install --user <remote> <package>
+

Packages to install:

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Flatpak dependencyChannel
com.github.wwmm.pulseeffectsflathub
com.discordapp.Discordflathub
com.jetbrains.DataGripflathub
chat.rocket.RocketChatflathub
+
(mapconcat
+ (lambda (c) (concat "flatpak install -y --user " (nth 1 c) " " (nth 0 c)))
+ table
+ "\n")
+

Nix

+ + + + + + + + + + + + + +
TypeDescription
TODOMake nix manifest?
+

I probably should’ve used nix, as almost every program I packaged so far exists in the Nix repo.

+

But it’s easy enough to use Nix on Guix.

+
https://nixos.org/channels/nixpkgs-unstable nixpkgs
+

Don’t forget to run the following after the first installation:

+
nix-channel --update
+

Installing packages:

+
nix-env -i slack
+

Services

+

GNU Shepherd is a service management system for GNU Guix.

+

I previously used supervisor, but shepherd also seems pretty capable.

+

Music

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryGuix dependency
musicmpd
musicncmpcpp
musicpicard
musicmpd-mpc
musicshntool
musiccuetools
musicflac
+

Music player daemon

+
(define mpd
+  (make <service>
+    #:provides '(mpd)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("mpd" "--no-daemon"))
+    #:stop (make-kill-destructor)))
+

MPD watcher

+
(define sqrt-data-agent-mpd
+  (make <service>
+    #:provides '(sqrt-data-agent-mpd)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("sqrt_data_agent_mpd"))
+    #:stop (make-kill-destructor)
+    #:requires '(mpd)))
+

GNU Mcron

+

GNU Mcron is a replacement for cron, written in Scheme.

+
(define mcron
+  (make <service>
+    #:provides '(mcron)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("mcron"))
+    #:stop (make-kill-destructor)))
+

ActivityWatch

+

ActivityWatch is a FOSS time tracker. It tracks screen and application usage and has integrations with browsers, Emacs, etc.

+ + + + + + + + + + + +
Guix dependency
activitywatch-bin
+

aw-server

+
(define aw-server
+  (make <service>
+    #:provides '(aw-server)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("aw-server"))
+    #:stop (make-kill-destructor)))
+

aw-watcher-afk has some problems with statup, so there is a wrapper script

+
sleep 5
+aw-watcher-afk
+

aw-watcher-afk

+
(define aw-watcher-afk
+  (make <service>
+    #:provides '(aw-watcher-afk)
+    #:requires '(aw-server)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("/home/pavel/bin/scripts/aw-watcher-afk-wrapper"))
+    #:stop (make-kill-destructor)))
+

aw-watcher-window

+
(define aw-watcher-window
+  (make <service>
+    #:provides '(aw-watcher-window)
+    #:requires '(aw-server)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("aw-watcher-window"))
+    #:stop (make-kill-destructor)))
+

PulseEffects

+
(define pulseeffects
+  (make <service>
+    #:provides '(pulseeffects)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("flatpak" "run" "com.github.wwmm.pulseeffects" "--gapplication-service"))
+    #:stop (make-kill-destructor)))
+

xsettingsd

+
(define xsettingsd
+  (make <service>
+    #:provides '(xsettingsd)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("xsettingsd"))
+    #:stop (make-kill-destructor)))
+

nm-applet

+
(define nm-applet
+  (make <service>
+    #:provides '(nm-applet)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("nm-applet"))
+    #:stop (make-kill-destructor)))
+

Discord rich presence

+

References:

+ + +
(define discord-rich-presence
+  (make <service>
+    #:provides '(discord-rich-presence)
+    #:one-shot? #t
+    #:start (make-system-constructor "ln -sf {app/com.discordapp.Discord,$XDG_RUNTIME_DIR}/discord-ipc-0")))
+

Polkit Authentication agent

+

Launch an authentication agent. Necessary for stuff like pkexec. I suspect I’m not doing that the intended way, but it seems to work.

+
(define polkit-gnome
+  (make <service>
+    #:provides '(polkit-gnome)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("/home/pavel/.guix-extra-profiles/desktop-misc/desktop-misc/libexec/polkit-gnome-authentication-agent-1"))
+    #:stop (make-kill-destructor)))
+

Xmodmap

+
(define xmodmap
+  (make <service>
+    #:provides '(xmodmap)
+    #:one-shot? #t
+    #:start (make-system-constructor "xmodmap /home/pavel/.Xmodmap")))
+

VPN

+

Run my OpenVPN setup. Not lauching this automatially, as it requires an active connection.

+
(define vpn
+  (make <service>
+    #:provides '(vpn)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("/home/pavel/bin/scripts/vpn-start"))
+    #:stop (make-kill-destructor)))
+

Davmail

+
(define davmail
+  (make <service>
+    #:provides '(davmail)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("/home/pavel/bin/davmail"))
+    #:stop (make-kill-destructor)))
+

vnstatd

+
(define vnstatd
+  (make <service>
+    #:provides '(vnstatd)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("vnstatd" "-n"))
+    #:stop (make-kill-destructor)))
+

opensnitch

+

opensnitch is a linux firewall.

+

Install it via nix:

+
nix-env -I opensnitchd opensnitch-ui
+

sudoers has to be modified this to work.

+
(define opensnitchd
+  (make <service>
+    #:provides '(opensnitchd)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("sudo" "opensnitchd"))
+    #:stop (make-kill-destructor)))
+
+(define opensnitch-ui
+  (make <service>
+    #:provides '(opensnitch-ui)
+    #:respawn? #t
+    #:start (make-forkexec-constructor '("sudo" "opensnitch-ui"))
+    #:stop (make-kill-destructor)))
+

Shepherd config

+

For some reason, running start on a one-shot service started to hang shepherd, not sure why… Turining these off for now.

+

Register services:

+
(register-services
+ mpd
+ sqrt-data-agent-mpd
+ mcron
+ aw-server
+ aw-watcher-afk
+ aw-watcher-window
+ pulseeffects
+ xsettingsd
+ ;; discord-rich-presence
+ polkit-gnome
+ vpn
+ davmail
+ ;; xmodmap
+ nm-applet
+ vnstatd
+ ;; opensnitchd
+ ;; opensnitch-ui
+ )
+

Daemonize shepherd

+
(action 'shepherd 'daemonize)
+

Run services

+
(for-each start '(mpd
+		  sqrt-data-agent-mpd
+		  mcron
+		  aw-server
+		  aw-watcher-afk
+		  aw-watcher-window
+		  pulseeffects
+		  xsettingsd
+		  ;; discord-rich-presence
+		  ;; polkit-gnome
+		  davmail
+		  ;; ; xmodmap
+		  ;; nm-applet
+		  vnstatd
+		  ;; opensnitchd
+		  ;; opensnitch-ui
+		  ))
+

Guix settings

+

Other desktop programs I use are listed below.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryGuix dependencyDescription
desktop-miscxpropTool to display properties of X windows
desktop-miscarandrGUI to xrandr
desktop-misclightControl screen brightness
desktop-miscponymixControl PulseAudio CLI
desktop-miscpavucontrolControl PulseAudio GUI
desktop-miscnetwork-manager-appletApplet to manage network connections
desktop-miscxmodmapProgram to modify keybindings on X server
desktop-miscfontconfig
desktop-miscpolkit-gnomePolkit authentication agent
desktop-miscfehImage viewer. Used to set background
desktop-misccopyqClipboard manager
desktop-miscthunarMy preferred GUI file manager
desktop-miscxdg-utilsgives xdg-open and stuff
desktop-miscgnome-font-viewerview fonts
desktop-miscqbittorrenttorrent client
desktop-miscanydeskRemote desktop software
desktop-miscgnome-disk-utilityManage disks
desktop-miscgpartedManage partitions
desktop-miscxevTest input
desktop-miscbluezProvides bluetoothctl
desktop-misctelegram-desktop
desktop-miscfont-google-noto-emoji
desktop-miscremmina
desktop-miscandroid-file-transfer
+

+
(my/format-guix-dependencies)
+
(specifications->manifest
+ '(
+   <<packages()>>))
+
+
+
+
+ Table of Contents + +
+ + +
+
+ +
+ + diff --git a/configs/emacs/index.html b/configs/emacs/index.html new file mode 100644 index 0000000..e674a98 --- /dev/null +++ b/configs/emacs/index.html @@ -0,0 +1,10317 @@ + + + + + + Emacs config + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ Emacs config + +

+
+

+ Emacs config + +

+
+

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.

+
+ +

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:

+
    +
  • 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: +
      +
    • Literate configuration
    • +
    • Interactive programming à la Jupyter Notebook
    • +
    • Task / project management
    • +
    • 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.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureLast commit
org-roam dailiesd2648918fcc338bd5c1cd6d5c0aa60a65077ccf7
org-roam projects025278a1e180e86f3aade20242e4ac1cdc1a2f13
treemacs3d87852745caacc0863c747f1fa9871d367240d2
tab-bar.el19ff54db9fe21fd5bdf404a8d2612176baa8a6f5
spaceline19ff54db9fe21fd5bdf404a8d2612176baa8a6f5
code compass8594d6f53e42c70bbf903e168607841854818a38
vue-mode8594d6f53e42c70bbf903e168607841854818a38
svelte-mode8594d6f53e42c70bbf903e168607841854818a38
pomidor8594d6f53e42c70bbf903e168607841854818a38
elfeed-score8e591e0d2afd909ae5be00caf17f9b17c6cd8b61
org-trello3f5967a5f63928ea9c8567d8d9f31e84cdbbc21f
jabber9b0e73a4703ff35a2d30fd704200052888191217
wallabag9b0e73a4703ff35a2d30fd704200052888191217
conda609fc84e439b11ea5064f3a948079daebb654aca
notmuch tags keybindingseac134c5456051171c1c777254f503cc71ce12cd
expand-regionab0d01c525f2b44dd64ec09747daf0fced4bd9c7
org-latex-impatientab0d01c525f2b44dd64ec09747daf0fced4bd9c7
dired-singleab0d01c525f2b44dd64ec09747daf0fced4bd9c7
progidyab0d01c525f2b44dd64ec09747daf0fced4bd9c7
tree-sitter1920a48aec49837d63fa88ca315928dc4e9d14c2
org-roam-protocol2f0c20eb01b8899d00d129cc7ca5c6b263c69c65
+

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.

+

References:

+ + +
(defvar bootstrap-version)
+(let ((bootstrap-file
+       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
+      (bootstrap-version 5))
+  (unless (file-exists-p bootstrap-file)
+    (with-current-buffer
+	(url-retrieve-synchronously
+	 "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
+	 'silent 'inhibit-cookies)
+      (goto-char (point-max))
+      (eval-print-last-sexp)))
+      (load bootstrap-file nil 'nomessage))
+

use-package

+

A macro to simplify package specification & configuration. Integrates with straight.el.

+

Set use-package-verbose to t to print out loading times for individual packages.

+

References:

+ + +
(straight-use-package 'use-package)
+(eval-when-compile (require 'use-package))
+

Variables & environment

+

This section is about optioning the Emacs config.

+

The following is true if Emacs is meant to be used with TRAMP over slow ssh. Take a look at the TRAMP section for more details.

+
(setq my/slow-ssh
+      (or
+       (string= (getenv "IS_TRAMP") "true")))
+

The following is true is Emacs is run on a remote server where I don’t need stuff like my org workflow

+
(setq my/remote-server
+      (or (string= (getenv "IS_REMOTE") "true")
+	  (string= (system-name) "dev-digital")
+	  (string= (system-name) "violet")
+	  (string= (system-name) "viridian")))
+

And the following is true if Emacs is run from termux on Android.

+
(setq my/is-termux (string-match-p (rx (* nonl) "com.termux" (* nonl)) (getenv "HOME")))
+

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.

+
<<minimal>>
+

To launch Emacs with this config, run

+
emacs -q -l ~/.emacs.d/init-minimal.el
+

A convinience macro:

+
(defmacro with-eval-after-load-norem (file &rest body)
+  (declare (indent 1) (debug (form def-body)))
+  `(unless my/remote-server
+     (with-eval-after-load ,file
+       ,@body)))
+

Performance

+

Measure startup speed

+

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.

+
(setq my/emacs-started nil)
+
+(add-hook 'emacs-startup-hook
+	  (lambda ()
+	    (message "*** Emacs loaded in %s with %d garbage collections."
+		     (format "%.2f seconds"
+			     (float-time
+			      (time-subtract after-init-time before-init-time)))
+		     gcs-done)
+	    (setq my/emacs-started t)))
+

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.

+
;; (setq use-package-verbose t)
+

Garbage collection

+

Just setting gc-cons-treshold to a larger value.

+
(setq gc-cons-threshold 80000000)
+(setq read-process-output-max (* 1024 1024))
+

Run garbage collection when Emacs is unfocused

+

Run GC when Emacs loses focus. Time will tell if that’s a good idea.

+

Some time has passed, and I still don’t know if there is any quantifiable advantage to this, but it doesn’t hurt.

+
(add-hook 'emacs-startup-hook
+	  (lambda ()
+	    (if (boundp 'after-focus-change-function)
+		(add-function :after after-focus-change-function
+			      (lambda ()
+				(unless (frame-focus-state)
+				  (garbage-collect))))
+	      (add-hook 'after-focus-change-function 'garbage-collect))))
+

Measure RAM usage

+

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.

+
(defun my/get-ram-usage-async (callback)
+  (let* ((temp-buffer (generate-new-buffer "*ps*"))
+	 (proc (start-process "ps" temp-buffer "ps"
+			      "-p" (number-to-string (emacs-pid)) "-o" "rss")))
+    (set-process-sentinel
+     proc
+     (lambda (process _msg)
+       (when (eq (process-status process) 'exit)
+	 (let* ((output (with-current-buffer temp-buffer
+			  (buffer-string)))
+		(usage (string-to-number (nth 1 (split-string output "\n")))))
+	   (ignore-errors
+	     (funcall callback usage)))
+	 (kill-buffer temp-buffer))))))
+
+(defun my/ram-usage ()
+  (interactive)
+  (my/get-ram-usage-async
+   (lambda (data)
+     (message "%f Gb" (/ (float data) 1024 1024)))))
+

Micromamba

+

mamba is a faster alternative to Anaconda, a package and environment manager. micromamba is a tiny version that provides a subset of mamba commands.

+

micromamba.el is my package to interact with the latter.

+
(use-package micromamba
+  :straight t
+  :if (executable-find "micromamba")
+  :config
+  (micromamba-activate "general"))
+

Config files

+

Custom file location

+

By default, custom writes stuff to init.el, which is somewhat annoying. The following makes it write to a separate file custom.el

+
(setq custom-file (concat user-emacs-directory "custom.el"))
+(load custom-file 'noerror)
+

authinfo

+

Use only the gpg-encrypted version of the file.

+
(setq auth-source-debug t)
+(setq auth-sources '("~/.authinfo"))
+

Private config

+

I have some variables which I don’t commit to the repo, e.g. my current location. They are stored in private.el

+
(let ((private-file (expand-file-name "private.el" user-emacs-directory)))
+  (when (file-exists-p private-file)
+    (load-file private-file)))
+

No littering

+

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-package no-littering
+  :straight t)
+

Prevent Emacs from closing

+

This adds a confirmation to avoid accidental Emacs closing.

+
(setq confirm-kill-emacs 'y-or-n-p)
+

General settings

+

Keybindings

+

general.el

+

general.el provides a convenient interface to manage Emacs keybindings.

+

References:

+ + +
(use-package general
+  :straight t
+  :config
+  (general-evil-setup))
+

which-key

+

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.

+

References:

+ + +
(use-package which-key
+  :config
+  (setq which-key-idle-delay 0.3)
+  (setq which-key-popup-type 'frame)
+  (which-key-mode)
+  (which-key-setup-side-window-bottom)
+  (set-face-attribute 'which-key-local-map-description-face nil
+		      :weight 'bold)
+  :straight t)
+
dump keybindings
+

A function to dump keybindings starting with a prefix to a buffer in a tree-like form.

+
(defun my/dump-bindings-recursive (prefix &optional level buffer)
+  (dolist (key (which-key--get-bindings (kbd prefix)))
+    (with-current-buffer buffer
+      (when level
+	(insert (make-string level ? )))
+      (insert (apply #'format "%s%s%s\n" key)))
+    (when (string-match-p
+	   (rx bos "+" (* nonl))
+	   (substring-no-properties (elt key 2)))
+      (my/dump-bindings-recursive
+       (concat prefix " " (substring-no-properties (car key)))
+       (+ 2 (or level 0))
+       buffer))))
+
+(defun my/dump-bindings (prefix)
+  "Dump keybindings starting with PREFIX in a tree-like form."
+  (interactive "sPrefix: ")
+  (let ((buffer (get-buffer-create "bindings")))
+    (with-current-buffer buffer
+      (erase-buffer))
+    (my/dump-bindings-recursive prefix 0 buffer)
+    (with-current-buffer buffer
+      (goto-char (point-min)))
+    (switch-to-buffer-other-window buffer)))
+

Evil

+

An entire ecosystem of packages that emulates the main features of Vim. Probably the best vim emulator out there.

+

The only problem is that the package name makes it hard to google anything by just typing “evil”.

+

References:

+ +
Evil-mode
+

Basic evil configuration.

+
(use-package evil
+  :straight t
+  :init
+  (setq evil-want-integration t)
+  (setq evil-want-C-u-scroll t)
+  (setq evil-want-keybinding nil)
+  (setq evil-search-module 'evil-search)
+  (setq evil-split-window-below t)
+  (setq evil-vsplit-window-right t)
+  (unless (display-graphic-p)
+    (setq evil-want-C-i-jump nil))
+  :config
+  (evil-mode 1)
+  ;; (setq evil-respect-visual-line-mode t)
+  (when (fboundp #'undo-tree-undo)
+    (evil-set-undo-system 'undo-tree))
+  (when (fboundp #'general-define-key)
+    (general-define-key
+     :states '(motion)
+     "ze" nil)))
+
Addons
+

evil-surround emulates one of my favorite vim plugins, surround.vim. Adds a lot of parentheses management options.

+
(use-package evil-surround
+  :straight t
+  :after evil
+  :config
+  (global-evil-surround-mode 1))
+

evil-commentary emulates commentary.vim. It provides actions for quick insertion and deletion of comments.

+
(use-package evil-commentary
+  :straight t
+  :after evil
+  :config
+  (evil-commentary-mode))
+

evil-quickscope emulates quickscope.vim. It highlights certain target characters for f, F, t, T keys.

+
(use-package evil-quickscope
+  :straight t
+  :after evil
+  :config
+  :hook ((prog-mode . turn-on-evil-quickscope-mode)
+	 (LaTeX-mode . turn-on-evil-quickscope-mode)
+	 (org-mode . turn-on-evil-quickscope-mode)))
+

evil-numbers allows incrementing and decrementing numbers at point.

+
(use-package evil-numbers
+  :straight t
+  :commands (evil-numbers/inc-at-pt evil-numbers/dec-at-pt)
+  :init
+  (general-nmap
+    "g+" 'evil-numbers/inc-at-pt
+    "g-" 'evil-numbers/dec-at-pt))
+

evil-lion provides alignment operators, somewhat similar to vim-easyalign.

+
(use-package evil-lion
+  :straight t
+  :config
+  (setq evil-lion-left-align-key (kbd "g a"))
+  (setq evil-lion-right-align-key (kbd "g A"))
+  (evil-lion-mode))
+

evil-matchit makes “%” to match things like tags. It doesn’t work perfectly, so I occasionally turn it off.

+
(use-package evil-matchit
+  :straight t
+  :disabled
+  :config
+  (global-evil-matchit-mode 1))
+
My additions
+

Do ex search in other buffer. Like *, but switch to other buffer and search there.

+
(defun my/evil-ex-search-word-forward-other-window (count &optional symbol)
+  (interactive (list (prefix-numeric-value current-prefix-arg)
+		     evil-symbol-word-search))
+  (save-excursion
+    (evil-ex-start-word-search nil 'forward count symbol))
+  (other-window 1)
+  (evil-ex-search-next))
+
+(general-define-key
+ :states '(normal)
+ "&" #'my/evil-ex-search-word-forward-other-window)
+
evil-collection
+

evil-collection is a package that provides evil bindings for a lot of different packages. One can see the complete list in the modes folder.

+
(use-package evil-collection
+  :straight t
+  :after evil
+  :config
+  (evil-collection-init
+   '(eww devdocs proced emms pass calendar dired ivy debug guix calc
+	 docker ibuffer geiser pdf info elfeed edebug bookmark company
+	 vterm flycheck profiler cider explain-pause-mode notmuch custom
+	 xref eshell helpful compile comint git-timemachine magit prodigy
+	 slime forge deadgrep vc-annonate telega doc-view gnus outline)))
+

Avy

+

Avy is a package that helps navigate Emacs in a tree-like manner.

+

References:

+ + +
(use-package avy
+  :straight t
+  :config
+  (setq avy-timeout-seconds 0.5)
+  (setq avy-ignored-modes
+	'(image-mode doc-view-mode pdf-view-mode exwm-mode))
+  (general-define-key
+   :states '(normal motion)
+   "-" nil
+   "--" #'avy-goto-char-2
+   "-=" #'avy-goto-symbol-1))
+

ace-link is a package to jump to links with avy.

+
(use-package ace-link
+  :straight t
+  :commands (ace-link-info ace-link-help ace-link-woman ace-link-eww))
+

My keybindings

+

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.

+
(defun minibuffer-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 (and delete-selection-mode transient-mark-mode mark-active)
+      (setq deactivate-mark  t)
+    (when (get-buffer "*Completions*") (delete-windows-on "*Completions*"))
+    (abort-recursive-edit)))
+
+(defun my/escape-key ()
+  (interactive)
+  (evil-ex-nohighlight)
+  (keyboard-quit))
+
+(general-define-key
+ :keymaps '(normal visual global)
+ [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)
+
Home & end
+
(general-def :states '(normal insert visual)
+  "<home>" 'beginning-of-line
+  "<end>" 'end-of-line)
+
My leader
+

Using the SPC key as a leader key, like in Doom Emacs or Spacemacs.

+
(general-create-definer my-leader-def
+  :keymaps 'override
+  :prefix "SPC"
+  :states '(normal motion emacs))
+
+(general-def :states '(normal motion emacs)
+  "SPC" nil
+  "M-SPC" (general-key "SPC"))
+
+(general-def :states '(insert)
+  "M-SPC" (general-key "SPC" :state 'normal))
+
+(my-leader-def "?" 'which-key-show-top-level)
+(my-leader-def "E" 'eval-expression)
+
+(general-def :states '(insert)
+  "<f1> e" #'eval-expression)
+

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.

+
(general-def
+  :keymaps 'universal-argument-map
+  "M-u" 'universal-argument-more)
+(general-def
+  :keymaps 'override
+  :states '(normal motion emacs insert visual)
+  "M-u" 'universal-argument)
+
Profiler
+

The built-in profiler is a magnificent tool to troubleshoot performance issues.

+
(my-leader-def
+  :infix "P"
+  "" '(:which-key "profiler")
+  "s" 'profiler-start
+  "e" 'profiler-stop
+  "p" 'profiler-report)
+
Buffer switching
+

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.

+
(general-define-key
+  :keymaps 'override
+  "C-<right>" 'evil-window-right
+  "C-<left>" 'evil-window-left
+  "C-<up>" 'evil-window-up
+  "C-<down>" 'evil-window-down
+  "C-h" 'evil-window-left
+  "C-l" 'evil-window-right
+  "C-k" 'evil-window-up
+  "C-j" 'evil-window-down
+  "C-x h" 'previous-buffer
+  "C-x l" 'next-buffer)
+
+(general-define-key
+ :keymaps 'evil-window-map
+ "x" 'kill-buffer-and-window
+ "d" 'kill-current-buffer)
+

winner-mode to keep the history of window states.

+

It doesn’t play too well with perspective.el, that is it has a single history list for all of the perspectives. But it is still quite usable.

+
(winner-mode 1)
+
+(general-define-key
+ :keymaps 'evil-window-map
+ "u" 'winner-undo
+ "U" 'winner-redo)
+
Buffer management
+
(my-leader-def
+  :infix "b"
+  "" '(:which-key "buffers")
+  "s" '((lambda () (interactive) (switch-to-buffer (persp-scratch-buffer)))
+	:which-key "*scratch*")
+  "m" '((lambda () (interactive) (persp-switch-to-buffer "*Messages*"))
+	:which-key "*Messages*")
+  "l" 'next-buffer
+  "h" 'previous-buffer
+  "k" 'kill-buffer
+  "b" 'persp-ivy-switch-buffer
+  "r" 'revert-buffer
+  "u" 'ibuffer)
+
xref
+

Some keybindings for xref and go to definition.

+
(general-nmap
+  "gD" 'xref-find-definitions-other-window
+  "gr" 'xref-find-references
+  "gd" 'evil-goto-definition)
+
+(my-leader-def
+  "fx" 'xref-find-apropos)
+
(use-package xref
+  :straight (:type built-in))
+
Folding
+

There are multiple ways to fold text in Emacs.

+

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.

+
(require 'hideshow)
+(general-define-key
+ :keymaps '(hs-minor-mode-map outline-minor-mode-map)
+ :states '(normal motion)
+ "ze" 'hs-hide-level
+ "TAB" 'evil-toggle-fold)
+
Zoom UI
+
(defun my/zoom-in ()
+  "Increase font size by 10 points"
+  (interactive)
+  (set-face-attribute 'default nil
+		      :height
+		      (+ (face-attribute 'default :height) 10)))
+
+(defun my/zoom-out ()
+  "Decrease font size by 10 points"
+  (interactive)
+  (set-face-attribute 'default nil
+		      :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:

+
if [[ $(xdotool getactivewindow getwindowname) =~ ^emacs(:.*)?@.* ]]; then
+    command="(my/emacs-i3-integration \"$@\")"
+    emacsclient -e "$command"
+else
+    i3-msg $@
+fi
+

This script is being run from the i3 configuration.

+

For this to work, we need to make sure that Emacs starts a server, so here is an expression to do just that:

+
(unless my/remote-server
+  (add-hook 'after-init-hook #'server-start))
+

And here is a simple macro to do the Emacs -> i3 part:

+
(defmacro i3-msg (&rest args)
+  `(start-process "emacs-i3-windmove" nil "i3-msg" ,@args))
+

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.

+
(defun my/emacs-i3-windmove (dir)
+  (let ((other-window (windmove-find-other-window dir)))
+    (if (or (null other-window) (window-minibuffer-p other-window))
+	(i3-msg "focus" (symbol-name dir))
+      (windmove-do-window-select dir))))
+

For the move I want the following behavior:

+
    +
  • 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.

+
(defun my/emacs-i3-direction-exists-p (dir)
+  (cl-some (lambda (dir)
+	  (let ((win (windmove-find-other-window dir)))
+	    (and win (not (window-minibuffer-p win)))))
+	(pcase dir
+	  ('width '(left right))
+	  ('height '(up down)))))
+

And the implementation of the move command.

+
(defun my/emacs-i3-move-window (dir)
+  (let ((other-window (windmove-find-other-window dir))
+	(other-direction (my/emacs-i3-direction-exists-p
+			  (pcase dir
+			    ('up 'width)
+			    ('down 'width)
+			    ('left 'height)
+			    ('right 'height)))))
+    (cond
+     ((and other-window (not (window-minibuffer-p other-window)))
+      (window-swap-states (selected-window) other-window))
+     (other-direction
+      (evil-move-window dir))
+     (t (i3-msg "move" (symbol-name dir))))))
+

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.

+
(defun my/emacs-i3-resize-window (dir kind value)
+  (if (or (one-window-p)
+	  (not (my/emacs-i3-direction-exists-p dir)))
+      (i3-msg "resize" (symbol-name kind) (symbol-name dir)
+	      (format "%s px or %s ppt" value value))
+    (setq value (/ value 2))
+    (pcase kind
+      ('shrink
+       (pcase dir
+	 ('width
+	  (evil-window-decrease-width value))
+	 ('height
+	  (evil-window-decrease-height value))))
+      ('grow
+       (pcase dir
+	 ('width
+	  (evil-window-increase-width value))
+	 ('height
+	  (evil-window-increase-height value)))))))
+

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.

+
(use-package transpose-frame
+  :straight t
+  :commands (transpose-frame))
+

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.

+
(defun my/emacs-i3-integration (command)
+  (pcase command
+    ((rx bos "focus")
+     (my/emacs-i3-windmove
+      (intern (elt (split-string command) 1))))
+    ((rx bos "move")
+     (my/emacs-i3-move-window
+      (intern (elt (split-string command) 1))))
+    ((rx bos "resize")
+     (my/emacs-i3-resize-window
+       (intern (elt (split-string command) 2))
+       (intern (elt (split-string command) 1))
+       (string-to-number (elt (split-string command) 3))))
+    ("layout toggle split" (transpose-frame))
+    ("split h" (evil-window-split))
+    ("split v" (evil-window-vsplit))
+    ("kill" (evil-quit))
+    (- (i3-msg command))))
+

Editing text

+

Various packages, tricks, and settings that help with the central task of Emacs - editing text.

+

Indentation & whitespace

+
Aggressive Indent
+

A package to keep the code intended.

+

Doesn’t work too well with many ecosystems because the LSP-based indentation is rather slow but nice for Lisps.

+

References:

+ + +
(use-package aggressive-indent
+  :commands (aggressive-indent-mode)
+  :straight t)
+
Delete trailing whitespace
+

Delete trailing whitespace on save, unless in particular modes where trailing whitespace is important, like Markdown.

+
(setq my/trailing-whitespace-modes '(markdown-mode))
+
+(require 'cl-extra)
+
+(add-hook 'before-save-hook
+	  (lambda ()
+	    (unless (cl-some #'derived-mode-p my/trailing-whitespace-modes)
+	      (delete-trailing-whitespace))))
+
Tabs
+

Some default settings to manage tabs.

+
(setq tab-always-indent nil)
+
+(setq-default default-tab-width 4)
+(setq-default tab-width 4)
+(setq-default evil-indent-convert-tabs nil)
+(setq-default indent-tabs-mode nil)
+(setq-default evil-shift-round nil)
+

Settings

+
Scrolling
+
(setq scroll-conservatively scroll-margin)
+(setq scroll-step 1)
+(setq scroll-preserve-screen-position t)
+(setq scroll-error-top-bottom t)
+(setq mouse-wheel-progressive-speed nil)
+(setq mouse-wheel-inhibit-click-time nil)
+
Clipboard
+
(setq select-enable-clipboard t)
+(setq mouse-yank-at-point t)
+
Backups
+
(setq backup-inhibited t)
+(setq auto-save-default nil)
+

Undo Tree

+

Replaces Emacs built-in sequential undo system with a tree-based one. Probably one of the greatest options of Emacs as a text editor.

+

References:

+ + +
(use-package undo-tree
+  :straight t
+  :config
+  (global-undo-tree-mode)
+  (evil-set-undo-system 'undo-tree)
+  (setq undo-tree-visualizer-diff t)
+  (setq undo-tree-visualizer-timestamps t)
+  (setq undo-tree-auto-save-history nil)
+
+  (my-leader-def "u" 'undo-tree-visualize)
+  (fset 'undo-auto-amalgamate 'ignore)
+  (setq undo-limit 6710886400)
+  (setq undo-strong-limit 100663296)
+  (setq undo-outer-limit 1006632960))
+

Snippets

+

A snippet system for Emacs and a collection of pre-built snippets.

+

yasnippet-snippets has to be loaded before yasnippet for user snippets to override the pre-built ones.

+

Edit <2022-04-11 Mon> I don’t really use yasnippet-snippets, so I’d rather write stuff manually.

+

References:

+ + +
(use-package yasnippet-snippets
+  :disabled
+  :straight t)
+
+(use-package yasnippet
+  :straight t
+  :config
+  (setq yas-snippet-dirs
+	`(,(concat (expand-file-name user-emacs-directory) "snippets")
+	  ;; yasnippet-snippets-dir
+	  ))
+  (setq yas-triggers-in-field t)
+  (yas-global-mode 1)
+  (my-leader-def
+    :keymaps 'yas-minor-mode-map
+    :infix "es"
+    "" '(:wk "yasnippet")
+    "n" #'yas-new-snippet
+    "s" #'yas-insert-snippet
+    "v" #'yas-visit-snippet-file))
+
+(general-imap "M-TAB" 'company-yasnippet)
+

Input Method

+
+

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.

+
+ +

Fortunately, Emacs offers a way out of the above with input methods.

+

References:

+ + +
(setq default-input-method "russian-computer")
+

M-x delete-horizontal-space doesn’t feel that useful to me.

+
(general-define-key
+ :keymaps 'global-map
+ "M-\\" #'toggle-input-method)
+

Other small packages

+
Managing parentheses (smartparens)
+

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.

+

References:

+ + +
(use-package smartparens
+  :straight t)
+
Visual fill column mode
+
(use-package visual-fill-column
+  :straight t
+  :commands (visual-fill-column-mode)
+  :config
+  (add-hook 'visual-fill-column-mode-hook
+	    (lambda () (setq visual-fill-column-center-text t))))
+
Accents
+

Input accented characters.

+
(use-package accent
+  :straight t
+  :init
+  (general-define-key
+   :states '(normal)
+   "gs" #'accent-menu)
+  (general-define-key
+   :states '(normal insert)
+   "M-n" #'accent-menu)
+  :commands (accent-menu)
+  :config
+  (general-define-key
+   :keymaps 'popup-menu-keymap
+   "C-j" #'popup-next
+   "C-k" #'popup-previous
+   "M-j" #'popup-next
+   "M-k" #'popup-previous)
+  (setq accent-custom '((a (ā))
+			(A (Ā)))))
+
binky
+

Experimenting with this package.

+
(use-package binky
+  :straight t
+  :init
+  (my-leader-def "j" #'binky-binky))
+

Working with projects

+

Packages related to managing projects.

+

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.

+
(use-package projectile
+  :straight t
+  :config
+  (projectile-mode +1)
+  (setq projectile-project-search-path '("~/Code" "~/Documents")))
+
+(use-package counsel-projectile
+  :after (counsel projectile)
+  :straight t)
+
+(my-leader-def
+  "p" '(:keymap projectile-command-map :which-key "projectile"))
+
+(general-nmap "C-p" 'counsel-projectile-find-file)
+

Git & Magit

+

Magit is a git interface for Emacs. The closest non-Emacs alternative (sans actual clones) I know is lazygit, which I used before Emacs.

+
(use-package magit
+  :straight t
+  :commands (magit-status magit-file-dispatch)
+  :init
+  (my-leader-def
+    "m" 'magit
+    "M" 'magit-file-dispatch)
+  :config
+  (setq magit-blame-styles
+	'((headings
+	   (heading-format . "%-20a %C %s\n"))
+	  (highlight
+	   (highlight-face . magit-blame-highlight))
+	  (lines
+	   (show-lines . t)
+	   (show-message . t)))))
+

forge provides integration with forges, such as GitHub and GitLab.

+
(use-package forge
+  :after magit
+  :straight t
+  :config
+  (add-to-list 'forge-alist '("gitlab.etu.ru"
+			      "gitlab.etu.ru/api/v4"
+			      "gitlab.etu.ru"
+			      forge-gitlab-repository)))
+

git-gutter is a package which shows git changes for each line (added/changed/deleted lines).

+
(use-package git-gutter
+  :straight t
+  :if (not my/slow-ssh)
+  :config
+  (global-git-gutter-mode +1))
+

git-timemachine allows visiting previous versions of a file.

+
(use-package git-timemachine
+  :straight t
+  :commands (git-timemachine))
+

Editorconfig

+

Editorconfig support for Emacs.

+

References:

+ + +
(use-package editorconfig
+  :straight t
+  :config
+  (unless my/slow-ssh (editorconfig-mode 1))
+  (add-to-list 'editorconfig-indentation-alist
+	       '(emmet-mode emmet-indentation)))
+

Editing files

+

A minor mode to remember recently edited files.

+
(recentf-mode 1)
+

Save the last place visited in the file.

+
(save-place-mode nil)
+

Deadgrep

+

deadgrep is a nice Emacs interface for ripgrep. Running ivy-occur in counsel-rg does something a bit similar, but the deadgrep is more full-featured.

+

Somehow I couldn’t hook toogle-truncate-lines into the existing package hooks, so here goes advice.

+
(defun my/deadgrep-fix-buffer-advice (fun &rest args)
+  (let ((buf (apply fun args)))
+    (with-current-buffer buf
+      (toggle-truncate-lines 1))
+    buf))
+
+(use-package deadgrep
+  :straight t
+  :commands (deadgrep)
+  :config
+  (advice-add #'deadgrep--buffer :around #'my/deadgrep-fix-buffer-advice))
+

Completion

+

Ivy, counsel, swiper

+

Minibuffer completion tools for Emacs.

+

References:

+ + +
(use-package ivy
+  :straight t
+  :config
+  (setq ivy-use-virtual-buffers t)
+  (ivy-mode))
+
+(use-package counsel
+  :straight t
+  :after ivy
+  :config
+  (counsel-mode))
+
+(use-package swiper
+  :defer t
+  :straight t)
+

ivy-rich

+

ivy-rich provides a more informative interface for ivy.

+
(use-package ivy-rich
+  :straight t
+  :after ivy
+  :config
+  (ivy-rich-mode 1)
+  (setcdr (assq t ivy-format-functions-alist) #'ivy-format-function-line))
+

prescient

+

A package that enhances sorting & filtering of candidates. ivy-prescient adds integration with Ivy.

+

References:

+ + +
(use-package ivy-prescient
+  :straight t
+  :after counsel
+  :config
+  (ivy-prescient-mode +1)
+  (setq ivy-prescient-retain-classic-highlighting t)
+  (prescient-persist-mode 1)
+  (setq ivy-prescient-sort-commands
+	'(:not swiper
+	       swiper-isearch
+	       ivy-switch-buffer
+	       ;; ivy-resume
+	       ;; ivy--restore-session
+	       lsp-ivy-workspace-symbol
+	       dap-switch-stack-frame
+	       my/dap-switch-stack-frame
+	       dap-switch-session
+	       dap-switch-thread
+	       counsel-grep
+	       ;; counsel-find-file
+	       counsel-git-grep
+	       counsel-rg
+	       counsel-ag
+	       counsel-ack
+	       counsel-fzf
+	       counsel-pt
+	       counsel-imenu
+	       counsel-yank-pop
+	       counsel-recentf
+	       counsel-buffer-or-recentf
+	       proced-filter-interactive
+	       proced-sort-interactive
+	       perspective-exwm-switch-perspective
+	       my/persp-ivy-switch-buffer-other-window
+	       lsp-execute-code-action
+	       dired-recent-open
+	       org-ql-view
+	       my/index-nav
+	       org-set-effort))
+  ;; Do not use prescient in find-file
+  (ivy--alist-set 'ivy-sort-functions-alist #'read-file-name-internal #'ivy-sort-file-function-default))
+

keybindings

+

Setting up quick access to various completions.

+
(my-leader-def
+  :infix "f"
+  "" '(:which-key "various completions")'
+  ;; "b" 'counsel-switch-buffer
+  "b" 'persp-ivy-switch-buffer
+  "e" 'micromamba-activate
+  "f" 'project-find-file
+  "c" 'counsel-yank-pop
+  "a" 'counsel-rg
+  "d" 'deadgrep
+  "A" 'counsel-ag)
+
+(general-define-key
+ :states '(insert normal)
+ "C-y" 'counsel-yank-pop)
+
+(defun my/swiper-isearch ()
+  (interactive)
+  (if current-prefix-arg
+      (swiper-all)
+    (swiper-isearch)))
+
+(my-leader-def "SPC SPC" 'ivy-resume)
+(my-leader-def "s" 'my/swiper-isearch)
+
+(general-define-key
+ :keymaps '(ivy-minibuffer-map swiper-map)
+ "M-j" 'ivy-next-line
+ "M-k" 'ivy-previous-line
+ "<C-return>" 'ivy-call
+ "M-RET" 'ivy-immediate-done
+ [escape] 'minibuffer-keyboard-quit)
+

company

+

A completion framework for Emacs.

+

References:

+ + +
(use-package company
+  :straight t
+  :config
+  (global-company-mode)
+  (setq company-idle-delay 0.125)
+  (setq company-dabbrev-downcase nil)
+  (setq company-show-numbers t))
+
+(general-imap "C-SPC" 'company-complete)
+

A company frontend with nice icons.

+

Disabled since the base company got icons support and since company-box has some issues with spaceline. Enabled back because I didn’t like spaceline.

+
(use-package company-box
+  :straight t
+  :if (display-graphic-p)
+  :after (company)
+  :hook (company-mode . company-box-mode))
+

Help

+
    +
  • CREDIT: Thanks @phundrak on the System Crafters Discord for suggesting help-map
  • +
+

helpful package improves the *help* buffer.

+
(use-package helpful
+  :straight t
+  :commands (helpful-callable
+	     helpful-variable
+	     helpful-key
+	     helpful-macro
+	     helpful-function
+	     helpful-command))
+

As I use C-h to switch buffers, I moved the help to SPC-h with the code below.

+
(my-leader-def
+  "h" '(:keymap help-map :which-key "help"))
+
+(my-leader-def
+  :infix "h"
+  "" '(:which-key "help")
+  "h" '(:keymap help-map :which-key "help-map")
+  "f" 'helpful-function
+  "k" 'helpful-key
+  "v" 'helpful-variable
+  "o" 'helpful-symbol
+  "i" 'info)
+
+(general-define-key
+ :keymaps 'help-map
+ "f" 'helpful-function
+ "k" 'helpful-key
+ "v" 'helpful-variable
+ "o" 'helpful-symbol)
+

Time trackers

+

Time trackers I happen to use.

+

References:

+ +

WakaTime

+

Before I figure out how to package this for Guix:

+
    +
  • Clone the repo
  • +
  • Run go build
  • +
  • Copy the binary to the ~/bin folder
  • +
+ +
(use-package wakatime-mode
+  :straight (:host github :repo "SqrtMinusOne/wakatime-mode")
+  :if (not (or my/remote-server))
+  :config
+  (setq wakatime-ignore-exit-codes '(0 1 102 112))
+  (advice-add 'wakatime-init :after (lambda () (setq wakatime-cli-path (expand-file-name "~/bin/wakatime-cli"))))
+  ;; (setq wakatime-cli-path (executable-find "wakatime"))
+  (global-wakatime-mode))
+

ActivityWatch

+
(use-package request
+  :straight t
+  :defer t)
+
+(use-package activity-watch-mode
+  :straight t
+  :if (not (or my/is-termux my/remote-server))
+  :config
+  (global-activity-watch-mode))
+

UI settings

+

General settings

+

Miscellaneous

+

Disable GUI elements

+
(unless my/is-termux
+  (tool-bar-mode -1)
+  (menu-bar-mode -1)
+  (scroll-bar-mode -1))
+
+(when my/is-termux
+  (menu-bar-mode -1))
+

Transparency. Not setting it here, as I used to use picom with i3, and EXWM config has its own settings.

+
;; (set-frame-parameter (selected-frame) 'alpha '(90 . 90))
+;; (add-to-list 'default-frame-alist '(alpha . (90 . 90)))
+

Prettify symbols. Also not setting it, ligatures seem to be enough for me.

+
;; (global-prettify-symbols-mode)
+

Do not show GUI dialogs

+
(setq use-dialog-box nil)
+

No start screen

+
(setq inhibit-startup-screen t)
+

Visual bell

+
(setq visible-bell 0)
+

y or n instead of yes or no

+
(defalias 'yes-or-no-p 'y-or-n-p)
+

Hide mouse cursor while typing

+
(setq make-pointer-invisible t)
+

Show pairs

+
(show-paren-mode 1)
+

Highlight the current line

+
(global-hl-line-mode 1)
+

Line numbers

+

Line numbers. There seems to be a catch with the relative number setting:

+
    +
  • visual doesn’t take folding into account but also doesn’t take wrapped lines into account (i.e. there are multiple numbers for a single wrapped line)
  • +
  • relative makes a single number for a wrapped line, but counts folded lines.
  • +
+

visual option seems to be less of a problem in most cases.

+
(global-display-line-numbers-mode 1)
+(line-number-mode nil)
+(setq display-line-numbers-type 'visual)
+(column-number-mode)
+

Word wrapping

+

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.
  • +
+ +
(setq word-wrap 1)
+(global-visual-line-mode 1)
+

Custom frame format

+

Title format, which used to look something like emacs:project@hostname. Now it’s just emacs.

+
(setq-default frame-title-format
+	      '(""
+		"emacs"
+		;; (:eval
+		;;  (let ((project-name (projectile-project-name)))
+		;;    (if (not (string= "-" project-name))
+		;;        (format ":%s@%s" project-name (system-name))
+		;;      (format "@%s" (system-name)))))
+		))
+

Olivetti

+

Olivetti is a package that limits the current text body width. It’s pretty nice to use when writing texts.

+
(use-package olivetti
+  :straight t
+  :if (display-graphic-p)
+  :config
+  (setq-default olivetti-body-width 86))
+

Keycast

+

Showing the last pressed key. Occasionally useful.

+
(use-package keycast
+  :straight t
+  :config
+  (define-minor-mode keycast-mode
+    "Keycast mode"
+    :global t
+    (if keycast-mode
+	(progn
+	  (add-to-list 'global-mode-string '("" keycast-mode-line " "))
+	  (add-hook 'pre-command-hook 'keycast--update t) )
+      (remove-hook 'pre-command-hook 'keycast--update)
+      (setq global-mode-string (delete '("" keycast-mode-line " ") global-mode-string)))))
+

Themes and colors

+

Theme packages

+

My colorschemes of choice.

+
(use-package doom-themes
+  :straight t
+  :config
+  (setq doom-themes-enable-bold t
+	doom-themes-enable-italic t)
+  ;; (if my/remote-server
+  ;;     (load-theme 'doom-gruvbox t)
+  ;;   (load-theme 'doom-palenight t))
+  (doom-themes-visual-bell-config)
+  (setq doom-themes-treemacs-theme "doom-colors")
+  (doom-themes-treemacs-config))
+
(use-package modus-themes
+  :straight t)
+

Let’s see…

+
(use-package ef-themes
+  :straight t
+  :config
+  (setq ef-duo-light-palette-overrides
+	'((constant green))))
+

Custom theme

+

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-package ct
+  :straight t)
+

As of now I want this to support doom-themes and modus-themes. So, let’s get which one is enabled:

+
(defun my/doom-p ()
+  (seq-find (lambda (x) (string-match-p (rx bos "doom") (symbol-name x)))
+	    custom-enabled-themes))
+
+(defun my/modus-p ()
+  (seq-find (lambda (x) (string-match-p (rx bos "modus") (symbol-name x)))
+	    custom-enabled-themes))
+
+(defun my/ef-p ()
+  (seq-find (lambda (x) (string-match-p (rx bos "ef") (symbol-name x)))
+	    custom-enabled-themes))
+

I also want to know if the current theme is light or not:

+
(defun my/light-p ()
+  (ct-light-p (my/color-value 'bg)))
+
+(defun my/dark-p ()
+  (not (my/light-p)))
+

Now, let’s get the current color from doom. doom-themes provide doom-color, but I also want to:

+
    +
  • override some colors
  • +
  • add black, white, light-* and border
  • +
+ +
(defconst my/theme-override
+  '((doom-palenight
+     (red . "#f07178"))))
+
+(defvar my/alpha-for-light 7)
+
+(defun my/doom-color (color)
+  (when (doom-color 'bg)
+    (let ((override (alist-get (my/doom-p) my/theme-override))
+	  (color-name (symbol-name color))
+	  (is-light (ct-light-p (doom-color 'bg))))
+      (or
+       (alist-get color override)
+       (cond
+	((eq 'black color)
+	 (if is-light (doom-color 'fg) (doom-color 'bg)))
+	((eq 'white color)
+	 (if is-light (doom-color 'bg) (doom-color 'fg)))
+	((eq 'border color)
+	 (if is-light (doom-color 'base0) (doom-color 'base8)))
+	((string-match-p (rx bos "light-") color-name)
+	 (ct-edit-hsl-l-inc (my/doom-color (intern (substring color-name 6)))
+			    my/alpha-for-light))
+	(t (doom-color color)))))))
+

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.

+
(defun my/modus-get-base (color)
+  (let ((base-value (string-to-number (substring (symbol-name color) 4 5)))
+	(base-start (cadr (assoc 'bg-main (modus-themes--current-theme-palette))))
+	(base-end (cadr (assoc 'fg-dim (modus-themes--current-theme-palette)))))
+    (nth base-value (ct-gradient 9 base-start base-end t))))
+
+(defun my/prot-color (color palette)
+  (let ((is-light (ct-light-p (cadr (assoc 'bg-main palette)))))
+    (cond
+     ((member color '(black white light-black light-white))
+      (let ((bg-main (cadr (assoc 'bg-main palette)))
+	    (fg-main (cadr (assoc 'fg-main palette))))
+	(pcase color
+	  ('black (if is-light fg-main bg-main))
+	  ('white (if is-light bg-main fg-main))
+	  ('light-black (ct-edit-hsl-l-inc
+			 (if is-light fg-main bg-main)
+			 15))
+	  ('light-white (ct-edit-hsl-l-inc
+			 (if is-light bg-main fg-main)
+			 15)))))
+     ((or (eq color 'bg))
+      (cadr (assoc 'bg-main palette)))
+     ((or (eq color 'fg))
+      (cadr (assoc 'fg-main palette)))
+     ((eq color 'bg-alt)
+      (cadr (assoc 'bg-dim palette)))
+     ((eq color 'violet)
+      (cadr (assoc 'magenta-cooler palette)))
+     ((string-match-p (rx bos "base" digit) (symbol-name color))
+      (my/modus-get-base color))
+     ((string-match-p (rx bos "dark-") (symbol-name color))
+      (cadr (assoc (intern (format "%s-cooler" (substring (symbol-name color) 5)))
+		   palette)))
+     ((eq color 'grey)
+      (my/modus-get-base 'base5))
+     ((string-match-p (rx bos "light-") (symbol-name color))
+      (or
+       (cadr (assoc (intern (format "%s-intense" (substring (symbol-name color) 6))) palette))
+       (cadr (assoc (intern (format "bg-%s-intense" (substring (symbol-name color) 6))) palette))))
+     (t (cadr (assoc color palette))))))
+
+(defun my/modus-color (color)
+  (my/prot-color color (modus-themes--current-theme-palette)))
+
+(defun my/ef-color (color)
+  (my/prot-color color (ef-themes--current-theme-palette)))
+

Test the three functions.

+
(defconst my/test-colors-list
+  '(black red green yellow blue magenta cyan white light-black
+	  light-red light-green light-yellow light-blue light-magenta
+	  light-cyan light-white bg fg violet grey base0 base1 base2
+	  base3 base4 base5 base6 base7 base8 border bg-alt))
+
+(defun my/test-colors ()
+  (interactive)
+  (let ((buf (generate-new-buffer "*colors-test*")))
+    (with-current-buffer buf
+      (insert (format "%-20s %-10s %-10s %-10s" "Color" "Doom" "Modus" "Ef") "\n")
+      (cl-loop for color in my/test-colors-list
+	       do (insert
+		   (format "%-20s %-10s %-10s %-10s\n"
+			   (prin1-to-string color)
+			   (my/doom-color color)
+			   (my/modus-color color)
+			   (my/ef-color color))))
+      (special-mode)
+      (rainbow-mode))
+    (switch-to-buffer buf)))
+

Finally, one function to get the value of a color in the current theme.

+
(defun my/color-value (color)
+  (cond
+   ((stringp color) (my/color-value (intern color)))
+   ((eq color 'bg-other)
+    (or (my/color-value 'bg-dim)
+	(let ((color (my/color-value 'bg)))
+	  (if (ct-light-p color)
+	      (ct-edit-hsl-l-dec color 2)
+	    (ct-edit-hsl-l-dec color 3)))))
+   ((my/doom-p) (my/doom-color color))
+   ((my/modus-p) (my/modus-color color))
+   ((my/ef-p) (my/ef-color color))))
+

And a few more functions

+
Custom theme
+

So, the custom theme:

+
(deftheme my-theme-1)
+

A macro to simplify defining custom colors.

+
(defvar my/my-theme-update-color-params nil)
+
+(defmacro my/use-colors (&rest data)
+  `(progn
+     ,@(cl-loop for i in data collect
+		`(setf (alist-get ',(car i) my/my-theme-update-color-params)
+		       (list ,@(cl-loop for (key value) on (cdr i) by #'cddr
+					append `(,key ',value)))))
+     (when (and (or (my/doom-p) (my/modus-p)) my/emacs-started)
+       (my/update-my-theme))))
+

This macro puts lambdas to my/my-theme-update-colors-hook that updates faces in my-theme-1. Now I have to call this hook:

+
(defun my/update-my-theme (&rest _)
+  (interactive)
+  (cl-loop for (face . values) in my/my-theme-update-color-params
+	   do (custom-theme-set-faces
+	       'my-theme-1
+	       `(,face ((t ,@(cl-loop for (key value) on values by #'cddr
+				      collect key
+				      collect (eval value)))))))
+  (enable-theme 'my-theme-1))
+
+(unless my/is-termux
+  (advice-add 'load-theme :after #'my/update-my-theme)
+  (add-hook 'emacs-startup-hook #'my/update-my-theme))
+

Defining colors for tab-bar.el:

+
(my/use-colors
+ (tab-bar-tab :background (my/color-value 'bg)
+	      :foreground (my/color-value 'yellow)
+	      :underline (my/color-value 'yellow))
+ (tab-bar :background nil :foreground nil)
+ (magit-section-secondary-heading :foreground (my/color-value 'blue)
+				  :weight 'bold))
+
Switch theme
+

The built-in load-theme does not deactivate the previous theme, so here’s a function that does that:

+
(defun my/switch-theme (theme)
+  (interactive
+   (list (intern (completing-read "Load custom theme: "
+				  (mapcar #'symbol-name
+							  (custom-available-themes))))))
+  (cl-loop for enabled-theme in custom-enabled-themes
+	   if (not (or (eq enabled-theme 'my-theme-1)
+		       (eq enabled-theme theme)))
+	   do (disable-theme enabled-theme))
+  (load-theme theme t)
+  (when current-prefix-arg
+    (my/regenerate-desktop)))
+
(my/switch-theme 'ef-duo-light)
+

Dim inactive buffers

+

Dim inactive buffers.

+
(use-package auto-dim-other-buffers
+  :straight t
+  :if (display-graphic-p)
+  :config
+  (auto-dim-other-buffers-mode t)
+  (my/use-colors
+   (auto-dim-other-buffers-face
+    :background (my/color-value 'bg-other))))
+

ANSI colors

+

ansi-color.el is a built-in Emacs package that translates ANSI color escape codes into faces.

+

It is used by many other packages but doesn’t seem to have an integration with doom-themes, so here is one.

+
(with-eval-after-load 'ansi-color
+  (my/use-colors
+   (ansi-color-black
+    :foreground (my/color-value 'base2) :background (my/color-value 'base0))
+   (ansi-color-red
+    :foreground (my/color-value 'red) :background (my/color-value 'red))
+   (ansi-color-green
+    :foreground (my/color-value 'green) :background (my/color-value 'green))
+   (ansi-color-yellow
+    :foreground (my/color-value 'yellow) :background (my/color-value 'yellow))
+   (ansi-color-blue
+    :foreground (my/color-value 'dark-blue) :background (my/color-value 'dark-blue))
+   (ansi-color-magenta
+    :foreground (my/color-value 'violet) :background (my/color-value 'violet))
+   (ansi-color-cyan
+    :foreground (my/color-value 'dark-cyan) :background (my/color-value 'dark-cyan))
+   (ansi-color-white
+    :foreground (my/color-value 'base8) :background (my/color-value 'base8))
+   (ansi-color-bright-black
+    :foreground (my/color-value 'base5) :background (my/color-value 'base5))
+   (ansi-color-bright-red
+    :foreground (my/color-value 'orange) :background (my/color-value 'orange))
+   (ansi-color-bright-green
+    :foreground (my/color-value 'teal) :background (my/color-value 'teal))
+   (ansi-color-bright-yellow
+    :foreground (my/color-value 'yellow) :background (my/color-value 'yellow))
+   (ansi-color-bright-blue
+    :foreground (my/color-value 'blue) :background (my/color-value 'blue))
+   (ansi-color-bright-magenta
+    :foreground (my/color-value 'magenta) :background (my/color-value 'magenta))
+   (ansi-color-bright-cyan
+    :foreground (my/color-value 'cyan) :background (my/color-value 'cyan))
+   (ansi-color-bright-white
+    :foreground (my/color-value 'fg) :background (my/color-value 'fg))))
+

Fonts

+

Frame font

+

To install a font, download the font and unpack it into the .local/share/fonts directory. Create one if it doesn’t exist.

+

As I use nerd fonts elsewhere, I use one in Emacs as well.

+

References:

+ + +
(when (display-graphic-p)
+  (if (x-list-fonts "JetBrainsMono Nerd Font")
+      (let ((font "-JB  -JetBrainsMono Nerd Font-medium-normal-normal-*-17-*-*-*-m-0-iso10646-1"))
+	(set-frame-font font nil t)
+	(add-to-list 'default-frame-alist `(font . ,font)))
+    (message "Install JetBrainsMono Nerd Font!")))
+

To make the icons work (e.g. in the Doom Modeline), run M-x all-the-icons-install-fonts. The package definition is somewhere later in the config.

+

Other fonts

+
(when (display-graphic-p)
+  (set-face-attribute 'variable-pitch nil :family "Cantarell" :height 1.0)
+  (set-face-attribute
+   'italic nil
+   :family "JetBrainsMono Nerd Font"
+   :weight 'regular
+   :slant 'italic))
+

Ligatures

+

Ligature setup for the JetBrainsMono font.

+
(use-package ligature
+  :straight (:host github :repo "mickeynp/ligature.el")
+  :if (display-graphic-p)
+  :config
+  (ligature-set-ligatures
+   '(
+     typescript-mode
+     typescript-ts-mode
+     js2-mode
+     javascript-ts-mode
+     vue-mode
+     svelte-mode
+     scss-mode
+     php-mode
+     python-mode
+     python-ts-mode
+     js-mode
+     markdown-mode
+     clojure-mode
+     go-mode
+     sh-mode
+     haskell-mode
+     web-mode)
+   '("--" "---" "==" "===" "!=" "!==" "=!=" "=:=" "=/=" "<="
+     ">=" "&&" "&&&" "&=" "++" "+++" "***" ";;" "!!" "??"
+     "?:" "?." "?=" "<:" ":<" ":>" ">:" "<>" "<<<" ">>>"
+     "<<" ">>" "||" "-|" "_|_" "|-" "||-" "|=" "||=" "##"
+     "###" "####" "#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:"
+     "#!" "#=" "^=" "<$>" "<$" "$>" "<+>" "<+" "+>" "<*>"
+     "<*" "*>" "</" "</>" "/>" "<!--" "<#--" "-->" "->" "->>"
+     "<<-" "<-" "<=<" "=<<" "<<=" "<==" "<=>" "<==>" "==>" "=>"
+     "=>>" ">=>" ">>=" ">>-" ">-" ">--" "-<" "-<<" ">->" "<-<"
+     "<-|" "<=|" "|=>" "|->" "<->" "<~~" "<~" "<~>" "~~" "~~>"
+     "~>" "~-" "-~" "~@" "[||]" "|]" "[|" "|}" "{|" "[<"
+     ">]" "|>" "<|" "||>" "<||" "|||>" "<|||" "<|>" "..." ".."
+     ".=" ".-" "..<" ".?" "::" ":::" ":=" "::=" ":?" ":?>"
+     "//" "///" "/*" "*/" "/=" "//=" "/==" "@_" "__"))
+  (global-ligature-mode t))
+

Icons

+

Run M-x all-the-icons-install-fonts at first setup.

+
(use-package all-the-icons
+  :if (display-graphic-p)
+  :straight t)
+

Text highlight

+

Highlight indent guides.

+
(use-package highlight-indent-guides
+  :straight t
+  :if (not (or my/remote-server))
+  :hook ((prog-mode . highlight-indent-guides-mode)
+	 (LaTeX-mode . highlight-indent-guides-mode))
+  :config
+  (setq highlight-indent-guides-method 'bitmap)
+  (setq highlight-indent-guides-bitmap-function 'highlight-indent-guides--bitmap-line))
+

Rainbow parentheses.

+
(use-package rainbow-delimiters
+  :straight t
+  :hook ((prog-mode . rainbow-delimiters-mode)))
+

Highlight colors

+
(use-package rainbow-mode
+  :commands (rainbow-mode)
+  :straight t)
+

Highlight TODOs and stuff

+
(use-package hl-todo
+  :hook (prog-mode . hl-todo-mode)
+  :straight t)
+

Doom Modeline

+

A modeline from Doom Emacs. A big advantage of this package is that it just works out of the box and does not require much customization.

+

I tried a bunch of other options, including spaceline, but in the end, decided that Doom Modeline works best for me.

+

References:

+ + +
(use-package doom-modeline
+  :straight t
+  ;; :if (not (display-graphic-p))
+  :init
+  (setq doom-modeline-env-enable-python nil)
+  (setq doom-modeline-env-enable-go nil)
+  (setq doom-modeline-buffer-encoding 'nondefault)
+  (setq doom-modeline-hud t)
+  (setq doom-modeline-persp-icon nil)
+  (setq doom-modeline-persp-name nil)
+  (setq doom-modeline-display-misc-in-all-mode-lines nil)
+  :config
+  (setq doom-modeline-minor-modes nil)
+  (setq doom-modeline-irc nil)
+  (setq doom-modeline-buffer-state-icon nil)
+  (doom-modeline-mode 1))
+

perspective.el

+

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.

+
(use-package perspective
+  :straight t
+  :init
+  ;; (setq persp-show-modestring 'header)
+  (setq persp-sort 'created)
+  (setq persp-suppress-no-prefix-key-warning t)
+  :config
+  (persp-mode)
+  (my-leader-def "x" '(:keymap perspective-map :which-key "perspective"))
+  (general-define-key
+   :keymaps 'override
+   :states '(normal emacs)
+   "gt" 'persp-next
+   "gT" 'persp-prev
+   "gn" 'persp-switch
+   "gN" 'persp-kill)
+  (general-define-key
+   :keymaps 'perspective-map
+   "b" 'persp-ivy-switch-buffer
+   "x" 'persp-ivy-switch-buffer
+   "u" 'persp-ibuffer))
+

Functions to manage buffers

+

Move the current buffer to a perspective and switch to it.

+
(defun my/persp-move-window-and-switch ()
+  (interactive)
+  (let* ((buffer (current-buffer)))
+    (call-interactively #'persp-switch)
+    (persp-set-buffer (buffer-name buffer))
+    (switch-to-buffer buffer)))
+

Copy the current buffer to a perspective and switch to it.

+
(defun my/persp-copy-window-and-switch ()
+  (interactive)
+  (let* ((buffer (current-buffer)))
+    (call-interactively #'persp-switch)
+    (persp-add-buffer (buffer-name buffer))
+    (switch-to-buffer buffer)))
+

Switch to a perspective buffer in other window.

+
(defun my/persp-ivy-switch-buffer-other-window (arg)
+  (interactive "P")
+  (declare-function ivy-switch-buffer-other-window "ivy.el")
+  (persp--switch-buffer-ivy-counsel-helper
+   arg
+   (lambda ()
+     (ivy-read "Switch to buffer in other window: " #'internal-complete-buffer
+	       :keymap ivy-switch-buffer-map
+	       :preselect (buffer-name (other-buffer (current-buffer)))
+	       :action #'ivy--switch-buffer-other-window-action
+	       :matcher #'ivy--switch-buffer-matcher
+	       :caller 'ivy-switch-buffer))))
+

Add keybindings to the default map.

+
(with-eval-after-load 'perspective
+  (general-define-key
+   :keymaps 'perspective-map
+   "m" #'my/persp-move-window-and-switch
+   "f" #'my/persp-copy-window-and-switch))
+

Automating perspectives

+

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.

+

First, let’s define a variable with “rules”:

+
(setq my/perspective-assign-alist '())
+

One rule looks as follows:

+
(major-mode workspace-index persp-name)
+

And a function to act on these rules.

+
(defvar my/perspective-assign-ignore nil
+  "If non-nil, ignore `my/perspective-assign'")
+
+(defun my/perspective-assign ()
+  (when-let* ((_ (not my/perspective-assign-ignore))
+	      (rule (alist-get major-mode my/perspective-assign-alist)))
+    (let ((workspace-index (car rule))
+	  (persp-name (cadr rule))
+	  (buffer (current-buffer)))
+      (if (fboundp #'perspective-exwm-assign-window)
+	  (progn
+	    (perspective-exwm-assign-window
+	     :workspace-index workspace-index
+	     :persp-name persp-name)
+	    (when workspace-index
+	      (exwm-workspace-switch workspace-index))
+	    (when persp-name
+	      (persp-switch persp-name)))
+	(with-perspective persp-name
+	  (persp-set-buffer buffer))
+	(persp-switch-to-buffer buffer)))))
+

Also advise to ignore the assignment:

+
(defun my/perspective-assign-ignore-advice (fun &rest args)
+  (let ((my/perspective-assign-ignore t))
+    (apply fun args)))
+

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.

+
(add-hook 'after-change-major-mode-hook #'my/perspective-assign)
+

And here is a simple macro to add rules to the list.

+
(defmacro my/persp-add-rule (&rest body)
+  (declare (indent 0))
+  (unless (= (% (length body) 3) 0)
+    (error "Malformed body in my/persp-add-rule"))
+  (let (result)
+    (while body
+      (let ((major-mode (pop body))
+	    (workspace-index (pop body))
+	    (persp-name (pop body)))
+	(push
+	 `(add-to-list 'my/perspective-assign-alist
+		       '(,major-mode . (,workspace-index ,persp-name)))
+	 result)))
+    `(progn
+       ,@result)))
+

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.

+
(defmacro my/command-in-persp (command-name persp-name workspace-index &rest args)
+  `'((lambda ()
+       (interactive)
+       (when (and ,workspace-index (fboundp #'exwm-workspace-switch-create))
+	 (exwm-workspace-switch-create ,workspace-index))
+       (persp-switch ,persp-name)
+       (delete-other-windows)
+       ,@args)
+     :wk ,command-name))
+

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.

+
(use-package treemacs
+  :straight t
+  :defer t
+  :config
+  ;; (setq treemacs-follow-mode nil)
+  ;; (setq treemacs-follow-after-init nil)
+  (setq treemacs-space-between-root-nodes nil)
+  ;; (treemacs-git-mode 'extended)
+  ;; (add-to-list 'treemacs-pre-file-insert-predicates #'treemacs-is-file-git-ignored?)
+  (general-define-key
+   :keymaps 'treemacs-mode-map
+   [mouse-1] #'treemacs-single-click-expand-action
+   "M-l" #'treemacs-root-down
+   "M-h" #'treemacs-root-up
+   "q" #'treemacs-quit)
+  (general-define-key
+   :keymaps 'treemacs-mode-map
+   :states '(normal emacs)
+   "q" 'treemacs-quit))
+
+(use-package treemacs-evil
+  :after (treemacs evil)
+  :straight t)
+

LSP

+

LSP-mode provides an IDE-like experience for Emacs - real-time diagnostics, code actions, intelligent autocompletion, etc.

+

References:

+ +
Setup
+
(use-package lsp-mode
+  :straight t
+  :if (not (or my/slow-ssh my/is-termux my/remote-server))
+  :hook (
+	 (typescript-mode . lsp)
+	 (js-mode . lsp)
+	 (vue-mode . lsp)
+	 (go-mode . lsp)
+	 (svelte-mode . lsp)
+	 ;; (python-mode . lsp)
+	 (json-mode . lsp)
+	 (haskell-mode . lsp)
+	 (haskell-literate-mode . lsp)
+	 (java-mode . lsp)
+	 ;; (csharp-mode . lsp)
+	 )
+  :commands lsp
+  :init
+  (setq lsp-keymap-prefix nil)
+  :config
+  (setq lsp-idle-delay 1)
+  (setq lsp-eslint-server-command '("node" "/home/pavel/.emacs.d/.cache/lsp/eslint/unzipped/extension/server/out/eslintServer.js" "--stdio"))
+  (setq lsp-eslint-run "onSave")
+  (setq lsp-signature-render-documentation nil)
+  ;; (lsp-headerline-breadcrumb-mode nil)
+  (setq lsp-headerline-breadcrumb-enable nil)
+  (setq lsp-modeline-code-actions-enable nil)
+  (setq lsp-modeline-diagnostics-enable nil)
+  (add-to-list 'lsp-language-id-configuration '(svelte-mode . "svelte")))
+
+(use-package lsp-ui
+  :straight t
+  :commands lsp-ui-mode
+  :config
+  (setq lsp-ui-doc-delay 2)
+  (setq lsp-ui-sideline-show-hover nil))
+
t
+
Integrations
+

The only integration left now is treemacs.

+

Origami should’ve leveraged LSP folding, but it was too unstable at the moment I tried it.

+
;; (use-package helm-lsp
+;;   :straight t
+;;   :commands helm-lsp-workspace-symbol)
+
+;; (use-package origami
+;;   :straight t
+;;   :hook (prog-mode . origami-mode))
+
+;; (use-package lsp-origami
+;;   :straight t
+;;   :config
+;;   (add-hook 'lsp-after-open-hook #'lsp-origami-try-enable))
+
+(use-package lsp-treemacs
+  :after (lsp)
+  :straight t
+  :commands lsp-treemacs-errors-list)
+
Keybindings
+
(my-leader-def
+  :infix "l"
+  "" '(:which-key "lsp")
+  "d" 'lsp-ui-peek-find-definitions
+  "r" 'lsp-rename
+  "u" 'lsp-ui-peek-find-references
+  "s" 'lsp-ui-find-workspace-symbol
+  "l" 'lsp-execute-code-action
+  "e" 'list-flycheck-errors)
+
UI
+

I don’t like how some language servers print the full filename in the progress indicator.

+
(defun my/lsp--progress-status ()
+  "Returns the status of the progress for the current workspaces."
+  (-let ((progress-status
+	  (s-join
+	   "|"
+	   (-keep
+	    (lambda (workspace)
+	      (let ((tokens (lsp--workspace-work-done-tokens workspace)))
+		(unless (ht-empty? tokens)
+		  (mapconcat
+		   (-lambda ((&WorkDoneProgressBegin :message? :title :percentage?))
+		     (concat (if percentage?
+				 (if (numberp percentage?)
+				     (format "%.0f%%%% " percentage?)
+				   (format "%s%%%% " percentage?))
+			       "")
+			     (let ((msg (url-unhex-string (or message\? title))))
+			       (if (string-match-p "\\`file:///" msg)
+				   (file-name-nondirectory msg)))))
+		   (ht-values tokens)
+		   "|"))))
+	    (lsp-workspaces)))))
+    (unless (s-blank? progress-status)
+      (concat lsp-progress-prefix progress-status))))
+
+(with-eval-after-load 'lsp-mode
+  (advice-add 'lsp--progress-status :override #'my/lsp--progress-status))
+

Flycheck

+

A syntax checking extension for Emacs. Integrates with LSP-mode, but can also use various standalone checkers.

+

References:

+ + +
(use-package flycheck
+  :straight t
+  :config
+  (global-flycheck-mode)
+  (setq flycheck-check-syntax-automatically '(save idle-buffer-switch mode-enabled))
+  ;; (add-hook 'evil-insert-state-exit-hook
+  ;;           (lambda ()
+  ;;             (if flycheck-checker
+  ;;                 (flycheck-buffer))
+  ;;             ))
+  (advice-add 'flycheck-eslint-config-exists-p :override (lambda() t))
+  (add-to-list 'display-buffer-alist
+	       `(,(rx bos "*Flycheck errors*" eos)
+		 (display-buffer-reuse-window
+		  display-buffer-in-side-window)
+		 (side            . bottom)
+		 (reusable-frames . visible)
+		 (window-height   . 0.33))))
+

General additional config

+

Have to put this before tree-sitter because I need my/set-smartparens-indent there.

+

Make smartparens behave the way I like for C-like languages.

+
(defun my/set-smartparens-indent (mode)
+  (sp-local-pair mode "{" nil :post-handlers '(("|| " "SPC") ("||\n[i]" "RET")))
+  (sp-local-pair mode "[" nil :post-handlers '(("|| " "SPC") ("||\n[i]" "RET")))
+  (sp-local-pair mode "(" nil :post-handlers '(("|| " "SPC") ("||\n[i]" "RET"))))
+

Override flycheck checker with eslint.

+
(defun my/set-flycheck-eslint()
+  "Override flycheck checker with eslint."
+  (setq-local lsp-diagnostic-package :none)
+  (setq-local flycheck-checker 'javascript-eslint))
+

Tree Sitter

+

Tree-Sitter integration with Emacs 29.

+

References:

+ + +
(use-package treesit
+  :straight (:type built-in)
+  :if (featurep 'treesit)
+  :config
+  (setq treesit-language-source-alist
+	'((bash "https://github.com/tree-sitter/tree-sitter-bash")
+	  (cmake "https://github.com/uyha/tree-sitter-cmake")
+	  (css "https://github.com/tree-sitter/tree-sitter-css")
+	  (elisp "https://github.com/Wilfred/tree-sitter-elisp")
+	  (go "https://github.com/tree-sitter/tree-sitter-go")
+	  (html "https://github.com/tree-sitter/tree-sitter-html")
+	  (javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
+	  (json "https://github.com/tree-sitter/tree-sitter-json")
+	  (make "https://github.com/alemuller/tree-sitter-make")
+	  (markdown "https://github.com/ikatyang/tree-sitter-markdown")
+	  (python "https://github.com/tree-sitter/tree-sitter-python")
+	  (toml "https://github.com/tree-sitter/tree-sitter-toml")
+	  (tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
+	  (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
+	  (yaml "https://github.com/ikatyang/tree-sitter-yaml")))
+  (setq treesit-font-lock-level 4)
+  (setq major-mode-remap-alist
+	'((typescript-mode . typescript-ts-mode)
+	  (js-mode . javascript-ts-mode)
+	  (python-mode . python-ts-mode)
+	  (json-mode . json-ts-mode)))
+  (cl-loop for (old-mode . new-mode) in major-mode-remap-alist
+	   do (my/set-smartparens-indent new-mode)
+	   do (set (intern (concat (symbol-name new-mode) "-hook"))
+		   (list
+		    (eval `(lambda ()
+			     (run-hooks
+			      ',(intern (concat (symbol-name old-mode) "-hook")))))))))
+

DAP

+

An Emacs client for Debugger Adapter Protocol.

+

Okay, so, I tried to use it many times… Chrome DevTools and ipdb / pudb are just better for me. Maybe I’ll check out RealGUD instead… Will see.

+

References:

+ + +
(use-package dap-mode
+  :straight t
+  :if (not (or my/remote-server my/is-termux))
+  :commands (dap-debug)
+  :init
+  (setq lsp-enable-dap-auto-configure nil)
+  :config
+
+  (setq dap-ui-variable-length 100)
+  (setq dap-auto-show-output nil)
+  (require 'dap-node)
+  (dap-node-setup)
+
+  (require 'dap-chrome)
+  (dap-chrome-setup)
+
+  (require 'dap-python)
+  (require 'dap-php)
+
+  (dap-mode 1)
+  (dap-ui-mode 1)
+  (dap-tooltip-mode 1)
+  (tooltip-mode 1))
+
Controls
+

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:

+
(with-eval-after-load 'dap-mode
+  (defmacro my/define-dap-ui-window-toggler (name)
+    `(defun ,(intern (concat "my/dap-ui-toggle-" name)) ()
+       ,(concat "Toggle DAP " name "buffer")
+       (interactive)
+       (if-let (window (get-buffer-window ,(intern (concat "dap-ui--" name "-buffer"))))
+	   (quit-window nil window)
+	 (,(intern (concat "dap-ui-" name))))))
+
+  (my/define-dap-ui-window-toggler "locals")
+  (my/define-dap-ui-window-toggler "expressions")
+  (my/define-dap-ui-window-toggler "sessions")
+  (my/define-dap-ui-window-toggler "breakpoints")
+  (my/define-dap-ui-window-toggler "repl"))
+

And here is the hydra:

+
(defhydra my/dap-hydra (:color pink :hint nil :foreign-keys run)
+  "
+^Stepping^         ^UI^                     ^Switch^                   ^Breakpoints^         ^Debug^                     ^Expressions
+^^^^^^^^------------------------------------------------------------------------------------------------------------------------------------------
+_n_: Next          _uc_: Controls           _ss_: Session              _bb_: Toggle          _dd_: Debug                 _ee_: Eval
+_i_: Step in       _ue_: Expressions        _st_: Thread               _bd_: Delete          _dr_: Debug recent          _er_: Eval region
+_o_: Step out      _ul_: Locals             _sf_: Stack frame          _ba_: Add             _dl_: Debug last            _es_: Eval thing at point
+_c_: Continue      _ur_: REPL               _su_: Up stack frame       _bc_: Set condition   _de_: Edit debug template   _ea_: Add expression
+_r_: Restart frame _uo_: Output             _sd_: Down stack frame     _bh_: Set hit count   _Q_:  Disconnect            _ed_: Remove expression
+		 _us_: Sessions           _sF_: Stack frame filtered _bl_: Set log message                           _eu_: Refresh expressions
+		 _ub_: Breakpoints                                                                               "
+
+  ("n" dap-next)
+  ("i" dap-step-in)
+  ("o" dap-step-out)
+  ("c" dap-continue)
+  ("r" dap-restart-frame)
+  ("uc" dap-ui-controls-mode)
+  ("ue" my/dap-ui-toggle-expressions)
+  ("ul" my/dap-ui-toggle-locals)
+  ("ur" my/dap-ui-toggle-repl)
+  ("uo" dap-go-to-output-buffer)
+  ("us" my/dap-ui-toggle-sessions)
+  ("ub" my/dap-ui-toggle-breakpoints)
+  ("ss" dap-switch-session)
+  ("st" dap-switch-thread)
+  ("sf" dap-switch-stack-frame)
+  ("sF" my/dap-switch-stack-frame)
+  ("su" dap-up-stack-frame)
+  ("sd" dap-down-stack-frame)
+  ("bb" dap-breakpoint-toggle)
+  ("ba" dap-breakpoint-add)
+  ("bd" dap-breakpoint-delete)
+  ("bc" dap-breakpoint-condition)
+  ("bh" dap-breakpoint-hit-condition)
+  ("bl" dap-breakpoint-log-message)
+  ("dd" dap-debug)
+  ("dr" dap-debug-recent)
+  ("dl" dap-debug-last)
+  ("de" dap-debug-edit-template)
+  ("ee" dap-eval)
+  ("ea" dap-ui-expressions-add)
+  ("er" dap-eval-region)
+  ("es" dap-eval-thing-at-point)
+  ("ed" dap-ui-expressions-remove)
+  ("eu" dap-ui-expressions-refresh)
+  ("q" nil "quit" :color blue)
+  ("Q" dap-disconnect :color red))
+
+(my-leader-def "d" #'my/dap-hydra/body)
+
UI Fixes
+

There are some problems with DAP UI in my setup.

+

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.

+
(defvar my/dap-mode-buffer-fixed nil)
+
+(with-eval-after-load 'dap-mode
+  (defmacro my/define-dap-tree-buffer-fixer (buffer-var buffer-name)
+    `(defun ,(intern (concat "my/fix-dap-ui-" buffer-name "-buffer")) (&rest _)
+       (with-current-buffer ,buffer-var
+	 (unless my/dap-mode-buffer-fixed
+	   (toggle-truncate-lines 1)
+	   (doom-modeline-set-modeline 'info)
+	   (setq-local my/dap-mode-buffer-fixed t)))))
+
+  (my/define-dap-tree-buffer-fixer dap-ui--locals-buffer "locals")
+  (my/define-dap-tree-buffer-fixer dap-ui--expressions-buffer "expressions")
+  (my/define-dap-tree-buffer-fixer dap-ui--sessions-buffer "sessions")
+  (my/define-dap-tree-buffer-fixer dap-ui--breakpoints-buffer "breakpoints")
+
+  (advice-add 'dap-ui-locals :after #'my/fix-dap-ui-locals-buffer)
+  (advice-add 'dap-ui-expressions :after #'my/fix-dap-ui-expressions-buffer)
+  (advice-add 'dap-ui-sessions :after #'my/fix-dap-ui-sessions-buffer)
+  (advice-add 'dap-ui-breakpoints :after #'my/fix-dap-ui-breakpoints-buffer))
+
Helper functions
+

Some helper functions that make debugging with DAP easier.

+

DAP seems to mess with window parameters from time to time. This function clears “bad” window parameters.

+
(defun my/clear-bad-window-parameters ()
+  "Clear window parameters that interrupt my workflow."
+  (interactive)
+  (let ((window (get-buffer-window (current-buffer))))
+    (set-window-parameter window 'no-delete-other-windows nil)))
+

A function to kill a value from a treemacs node.

+
(defun my/dap-yank-value-at-point (node)
+  (interactive (list (treemacs-node-at-point)))
+  (kill-new (message (plist-get (button-get node :item) :value))))
+

A function to open a value from a treemacs node in a new buffer.

+
(defun my/dap-display-value (node)
+  (interactive (list (treemacs-node-at-point)))
+  (let ((value (plist-get (button-get node :item) :value)))
+    (when value
+      (let ((buffer (generate-new-buffer "dap-value")))
+	(with-current-buffer buffer
+	  (insert value))
+	(select-window (display-buffer buffer))))))
+
Switch to stack frame with filter
+

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:

+
(with-eval-after-load 'dap-mode
+  (setq my/dap-stack-frame-filters
+	`(("node_modules,node:internal" . ,(rx (or "node_modules" "node:internal")))
+	  ("node_modules" . ,(rx (or "node_modules")))
+	  ("node:internal" . ,(rx (or "node:internal")))))
+
+  (setq my/dap-stack-frame-current-filter (cdar my/dap-stack-frame-filters))
+
+  (defun my/dap-stack-frame-filter-set ()
+    (interactive)
+    (setq my/dap-stack-frame-current-filter
+	  (cdr
+	   (assoc
+	    (completing-read "Filter: " my/dap-stack-frame-filters)
+	    my/dap-stack-frame-filters))))
+
+  (defun my/dap-stack-frame-filter (frame)
+    (when-let (path (dap--get-path-for-frame frame))
+      (not (string-match my/dap-stack-frame-current-filter path)))))
+

And here is a version of dap-switch-stack-frame that uses the said filter.

+
(defun my/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* ((index 0)
+		 (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-frame frame))
+			 (format "%s: %s (in %s)"
+				 (cl-incf index) name frame-path)
+		       (format "%s: %s" (cl-incf index) 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:

+
(defun my/exwm-perspective-find-buffer (path)
+  "Find a buffer with PATH in all EXWM perspectives.
+
+Returns (<buffer> . <workspace-index>) or nil."
+  (let* ((buf (cl-loop for buf being buffers
+		       if (and (buffer-file-name buf)
+			       (f-equal-p (buffer-file-name buf) path))
+		       return buf))
+	 (target-workspace
+	  (and buf
+	       (cl-loop for frame in exwm-workspace--list
+			if (with-selected-frame frame
+			     (cl-loop for persp-name being the hash-keys of (perspectives-hash)
+				      if (member buf (persp-buffers
+						      (gethash persp-name (perspectives-hash))))
+				      return persp-name))
+			return (cl-position frame exwm-workspace--list)))))
+    (when target-workspace (cons buf target-workspace))))
+

And override dap--go-to-stack-frame to take that into account. For some reason, evaluating this before dap-mode doesn’t work.

+
(defun my/dap--go-to-stack-frame-override (debug-session stack-frame)
+  "Make STACK-FRAME the active STACK-FRAME of DEBUG-SESSION."
+  (with-lsp-workspace (dap--debug-session-workspace debug-session)
+    (when stack-frame
+      (-let* (((&hash "line" line "column" column "name" name) stack-frame)
+	      (path (dap--get-path-for-frame stack-frame)))
+	(setf (dap--debug-session-active-frame debug-session) stack-frame)
+	;; If we have a source file with path attached, open it and
+	;; position the point in the line/column referenced in the
+	;; stack trace.
+	(if (and path (file-exists-p path))
+	    (progn
+	      (let ((exwm-target (my/exwm-perspective-find-buffer path)))
+		(if exwm-target
+		    (progn
+		      (unless (= (cdr exwm-target) exwm-workspace-current-index)
+			(exwm-workspace-switch (cdr exwm-target)))
+		      (persp-switch-to-buffer (car exwm-target)))
+		  (select-window (get-mru-window (selected-frame) nil))
+		  (find-file path)))
+	      (goto-char (point-min))
+	      (forward-line (1- line))
+	      (forward-char column))
+	  (message "No source code for %s. Cursor at %s:%s." name line column))))
+    (run-hook-with-args 'dap-stack-frame-changed-hook debug-session)))
+
+(with-eval-after-load 'exwm
+  (with-eval-after-load 'dap-mode
+    (advice-add #'dap--go-to-stack-frame :override #'my/dap--go-to-stack-frame-override)))
+
+;; (advice-remove #'dap--go-to-stack-frame #'my/dap--go-to-stack-frame-override)
+
Debug templates
+

Some debug templates I frequently use.

+
(with-eval-after-load 'dap-mode
+  (dap-register-debug-template
+   "Node::Nest.js"
+   (list :type "node"
+	 :request "attach"
+	 :name "Node::Attach"
+	 :port 9229
+	 :outFiles ["${workspaceFolder}/dist/**/*.js"]
+	 :sourceMaps t
+	 :program "${workspaceFolder}/src/app.ts"))
+  (dap-register-debug-template
+   "Node::Babel"
+   (list :type "node"
+	 :request "attach"
+	 :name "Node::Attach"
+	 :port 9229
+	 :program "${workspaceFolder}/dist/bin/www.js")))
+

Reformatter

+

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-package reformatter
+  :straight t)
+

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.

+
(defun my/copilot-tab ()
+  (interactive)
+  (or (copilot-accept-completion)
+      (when (my/should-run-emmet-p) (my/emmet-or-tab))
+      (when (and (eq evil-state 'normal)
+		 (or hs-minor-mode outline-minor-mode))
+	(evil-toggle-fold)
+	t)
+      (indent-for-tab-command)))
+
+(use-package copilot
+  :straight (:host github :repo "SqrtMinusOne/copilot.el" :files ("dist" "*.el"))
+  :commands (copilot-mode)
+  :if (not (or my/remote-server my/is-termux))
+  :init
+  (add-hook 'prog-mode-hook #'copilot-mode)
+  :config
+  (setq copilot-node-executable "/home/pavel/.guix-extra-profiles/dev/dev/bin/node")
+  (general-define-key
+   :keymaps 'company-active-map
+   "<backtab>" #'my/copilot-tab)
+  (general-define-key
+   :keymaps 'copilot-mode-map
+   "<tab>" #'my/copilot-tab
+   "M-j" #'copilot-accept-completion-by-line
+   "M-l" #'copilot-accept-completion-by-word)
+  (setq copilot-lispy-integration t))
+

Web development

+

Configs for various web development technologies I’m using.

+

Emmet

+

Emmet is a toolkit which greatly speeds up typing HTML & CSS.

+ + + + + + + + + + + + + +
TypeNote
TODOmake expand div[disabled] as <div disabled></div>
+

My bit of config here:

+
    +
  • makes TAB the only key I have to use
  • +
+ +
(defun my/should-run-emmet-p ()
+  (and (bound-and-true-p emmet-mode)
+       (or (and (derived-mode-p 'web-mode)
+		(member (web-mode-language-at-pos) '("html" "css")))
+	   (not (derived-mode-p 'web-mode)))))
+
+(use-package emmet-mode
+  :straight t
+  :hook ((vue-html-mode . emmet-mode)
+	 (svelte-mode . emmet-mode)
+	 (web-mode . emmet-mode)
+	 (html-mode . emmet-mode)
+	 (css-mode . emmet-mode)
+	 (scss-mode . emmet-mode))
+  :config
+  (defun my/emmet-or-tab (&optional arg)
+    (interactive)
+    (if (my/should-run-emmet-p)
+	(or (emmet-expand-line arg)
+	    (emmet-go-to-edit-point 1)
+	    (indent-for-tab-command arg))
+      (indent-for-tab-command arg)))
+  (general-imap :keymaps 'emmet-mode-keymap
+    "TAB" 'my/emmet-or-tab
+    "<backtab>" 'emmet-prev-edit-point))
+

Prettier

+
(use-package prettier
+  :commands (prettier-prettify)
+  :straight t
+  :init
+  (my-leader-def
+    :keymaps '(js-mode-map
+	       web-mode-map
+	       typescript-mode-map
+	       typescript-ts-mode-map
+	       vue-mode-map
+	       svelte-mode-map)
+    "rr" #'prettier-prettify))
+

TypeScript

+
(use-package typescript-mode
+  :straight t
+  :mode "\\.ts\\'"
+  :init
+  (add-hook 'typescript-mode-hook #'smartparens-mode)
+  (add-hook 'typescript-mode-hook #'rainbow-delimiters-mode)
+  (add-hook 'typescript-mode-hook #'hs-minor-mode)
+  :config
+  (my/set-smartparens-indent 'typescript-mode))
+

JavaScript

+
(add-hook 'js-mode-hook #'smartparens-mode)
+(add-hook 'js-mode-hook #'hs-minor-mode)
+(my/set-smartparens-indent 'js-mode)
+

Jest

+
(use-package jest-test-mode
+  :straight t
+  :hook ((typescript-mode . jest-test-mode)
+	 (js-mode . jest-test-mode))
+  :config
+  (my-leader-def
+    :keymaps 'jest-test-mode-map
+    :infix "t"
+    "t" #'jest-test-run-at-point
+    "d" #'jest-test-debug-run-at-point
+    "r" #'jest-test-run
+    "a" #'jest-test-run-all-tests)
+  (defmacro my/jest-test-with-debug-flags (form)
+    "Execute FORM with debugger flags set."
+    (declare (indent 0))
+    `(let ((jest-test-options (seq-concatenate 'list jest-test-options (list "--runInBand") ))
+	   (jest-test-npx-options (seq-concatenate 'list jest-test-npx-options (list "--node-options" "--inspect-brk"))))
+       ,form))
+  (defun my/jest-test-debug ()
+    "Run the test with an inline debugger attached."
+    (interactive)
+    (my/jest-test-with-debug-flags
+      (jest-test-run)))
+  (defun my/jest-test-debug-rerun-test ()
+    "Run the test with an inline debugger attached."
+    (interactive)
+    (my/jest-test-with-debug-flags
+      (jest-test-rerun-test)))
+  (defun my/jest-test-debug-run-at-point ()
+    "Run the test with an inline debugger attached."
+    (interactive)
+    (my/jest-test-with-debug-flags
+      (jest-test-run-at-point)))
+  (advice-add #'jest-test-debug :override #'my/jest-test-debug)
+  (advice-add #'jest-test-debug-rerun-test :override #'my/jest-test-debug-rerun-test)
+  (advice-add #'jest-test-debug-run-at-point
+	      :override #'my/jest-test-debug-run-at-point))
+
(defun my/jest-test-run-at-point-copy ()
+  "Run the top level describe block of the current buffer's point."
+  (interactive)
+  (let ((filename (jest-test-find-file))
+	(example  (jest-test-unit-at-point)))
+    (if (and filename example)
+	(jest-test-from-project-directory filename
+	  (let ((jest-test-options (seq-concatenate 'list jest-test-options (list "-t" example))))
+	    (kill-new (jest-test-command filename))))
+      (message jest-test-not-found-message))))
+

web-mode

+

web-mode.el is a major mode to edit various web templates.

+

Trying this one out instead of vue-mode and svelte-mode, because this one seems to have better support for tree-sitter and generally less problems.

+

Set web-mode-auto-pairs not nil because smartparens already fulfills that role.

+
(use-package web-mode
+  :straight t
+  :commands (web-mode)
+  :init
+  (add-to-list 'auto-mode-alist '("\\.svelte\\'" . web-mode))
+  (add-to-list 'auto-mode-alist '("\\.vue\\'" . web-mode))
+  :config
+  (add-hook 'web-mode-hook 'smartparens-mode)
+  (add-hook 'web-mode-hook 'hs-minor-mode)
+  (my/set-smartparens-indent 'web-mode)
+  (setq web-mode-auto-pairs nil))
+

Hooking this up with lsp.

+
(setq my/web-mode-lsp-extensions
+      `(,(rx ".svelte" eos)
+	,(rx ".vue" eos)))
+
+(defun my/web-mode-lsp ()
+  (when (seq-some
+	 (lambda (regex) (string-match-p regex (buffer-name)))
+	 my/web-mode-lsp-extensions)
+    (lsp-deferred)))
+
+(add-hook 'web-mode-hook #'my/web-mode-lsp)
+

Vue settings

+
(defun my/web-mode-vue-setup (&rest _)
+  (when (string-match-p (rx ".vue" eos) (buffer-name))
+    (setq-local web-mode-script-padding 0)
+    (setq-local web-mode-style-padding 0)
+    (setq-local create-lockfiles nil)
+    (setq-local web-mode-enable-auto-pairing nil)))
+
+(add-hook 'web-mode-hook 'my/web-mode-vue-setup)
+(add-hook 'editorconfig-after-apply-functions 'my/web-mode-vue-setup)
+

SCSS

+
(add-hook 'scss-mode-hook #'smartparens-mode)
+(add-hook 'scss-mode-hook #'hs-minor-mode)
+(my/set-smartparens-indent 'scss-mode)
+

PHP

+
(use-package php-mode
+  :straight t
+  :mode "\\.php\\'"
+  :config
+  (add-hook 'php-mode-hook #'smartparens-mode)
+  (add-hook 'php-mode-hook #'lsp)
+  (my/set-smartparens-indent 'php-mode))
+

LaTeX

+

AUCTeX

+

The best LaTeX editing environment I’ve found so far.

+

References:

+ + +
(use-package tex
+  :straight auctex
+  :defer t
+  :config
+  (setq-default TeX-auto-save t)
+  (setq-default TeX-parse-self t)
+  (TeX-PDF-mode)
+  ;; Use XeLaTeX & stuff
+  (setq-default TeX-engine 'xetex)
+  (setq-default TeX-command-extra-options "-shell-escape")
+  (setq-default TeX-source-correlate-method 'synctex)
+  (TeX-source-correlate-mode)
+  (setq-default TeX-source-correlate-start-server t)
+  (setq-default LaTeX-math-menu-unicode t)
+
+  (setq-default font-latex-fontify-sectioning 1.3)
+
+  ;; Scale preview for my DPI
+  (setq-default preview-scale-function 1.4)
+  (when (boundp 'tex--prettify-symbols-alist)
+    (assoc-delete-all "--" tex--prettify-symbols-alist)
+    (assoc-delete-all "---" tex--prettify-symbols-alist))
+
+  (add-hook 'LaTeX-mode-hook
+	    (lambda ()
+	      (TeX-fold-mode 1)
+	      (outline-minor-mode)))
+
+  (add-to-list 'TeX-view-program-selection
+	       '(output-pdf "Zathura"))
+
+  ;; Do not run lsp within templated TeX files
+  (add-hook 'LaTeX-mode-hook
+	    (lambda ()
+	      (unless (string-match "\.hogan\.tex$" (buffer-name))
+		(lsp))
+	      (setq-local lsp-diagnostic-package :none)
+	      (setq-local flycheck-checker 'tex-chktex)))
+
+  (add-hook 'LaTeX-mode-hook #'rainbow-delimiters-mode)
+  (add-hook 'LaTeX-mode-hook #'smartparens-mode)
+  (add-hook 'LaTeX-mode-hook #'prettify-symbols-mode)
+
+  (my/set-smartparens-indent 'LaTeX-mode)
+  (require 'smartparens-latex)
+
+  (general-nmap
+    :keymaps '(LaTeX-mode-map latex-mode-map)
+    "RET" 'TeX-command-run-all
+    "C-c t" 'orgtbl-mode)
+
+  <<init-greek-latex-snippets>>
+  <<init-english-latex-snippets>>
+  <<init-math-latex-snippets>>
+  <<init-section-latex-snippets>>)
+

Import *.sty

+

A function to import .sty files to the LaTeX document.

+
(defun my/list-sty ()
+  (reverse
+   (sort
+    (seq-filter
+     (lambda (file) (if (string-match ".*\.sty$" file) 1 nil))
+     (directory-files
+      (seq-some
+       (lambda (dir)
+	 (if (and
+	      (f-directory-p dir)
+	      (seq-some
+	       (lambda (file) (string-match ".*\.sty$" file))
+	       (directory-files dir))
+	      ) dir nil))
+       (list "./styles" "../styles/" "." "..")) :full))
+    (lambda (f1 f2)
+      (let ((f1b (file-name-base f1))
+	    (f1b (file-name-base f2)))
+	(cond
+	 ((string-match-p ".*BibTex" f1) t)
+	 ((and (string-match-p ".*Locale" f1) (not (string-match-p ".*BibTex" f2))) t)
+	 ((string-match-p ".*Preamble" f2) t)
+	 (t (string-lessp f1 f2))))))))
+
+(defun my/import-sty ()
+  (interactive)
+  (insert
+   (apply #'concat
+	  (cl-mapcar
+	   (lambda (file) (concat "\\usepackage{" (file-name-sans-extension (file-relative-name file default-directory)) "}\n"))
+	   (my/list-sty)))))
+
+(defun my/import-sty-org ()
+  (interactive)
+  (insert
+   (apply #'concat
+	  (cl-mapcar
+	   (lambda (file) (concat "#+LATEX_HEADER: \\usepackage{" (file-name-sans-extension (file-relative-name file default-directory)) "}\n"))
+	   (my/list-sty)))))
+

Snippets

+ + + + + + + + + + + + + +
NoteType
TODOMove yasnippet snippets here? Maybe extract to a separate file?
+
Greek letters
+

Autogenerate snippets for greek letters. I have a few blocks like this because it’s faster & more flexible than usual yasnippet snippets.

+

Noweb points to the AUCTeX config block.

+
(setq my/greek-alphabet
+      '(("a" . "\\alpha")
+	("b" . "\\beta" )
+	("g" . "\\gamma")
+	("d" . "\\delta")
+	("e" . "\\epsilon")
+	("z" . "\\zeta")
+	("h" . "\\eta")
+	("o" . "\\theta")
+	("i" . "\\iota")
+	("k" . "\\kappa")
+	("l" . "\\lambda")
+	("m" . "\\mu")
+	("n" . "\\nu")
+	("x" . "\\xi")
+	("p" . "\\pi")
+	("r" . "\\rho")
+	("s" . "\\sigma")
+	("t" . "\\tau")
+	("u" . "\\upsilon")
+	("f" . "\\phi")
+	("c" . "\\chi")
+	("v" . "\\psi")
+	("g" . "\\omega")))
+
+(setq my/latex-greek-prefix "'")
+
+;; The same for capitalized letters
+(dolist (elem my/greek-alphabet)
+  (let ((key (car elem))
+	(value (cdr elem)))
+    (when (string-equal key (downcase key))
+      (add-to-list 'my/greek-alphabet
+		   (cons
+		    (capitalize (car elem))
+		    (concat
+		     (substring value 0 1)
+		     (capitalize (substring value 1 2))
+		     (substring value 2)))))))
+
+(yas-define-snippets
+ 'latex-mode
+ (mapcar
+  (lambda (elem)
+    (list (concat my/latex-greek-prefix (car elem)) (cdr elem) (concat "Greek letter " (car elem))))
+  my/greek-alphabet))
+
English letters
+
(setq my/english-alphabet
+      '("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z"))
+
+(dolist (elem my/english-alphabet)
+  (when (string-equal elem (downcase elem))
+    (add-to-list 'my/english-alphabet (upcase elem))))
+
+(setq my/latex-mathbb-prefix "`")
+
+(yas-define-snippets
+ 'latex-mode
+ (mapcar
+  (lambda (elem)
+    (list (concat my/latex-mathbb-prefix elem) (concat "\\mathbb{" elem "}") (concat "Mathbb letter " elem)))
+  my/english-alphabet))
+
Math symbols
+
(setq my/latex-math-symbols
+      '(("x" . "\\times")
+	("." . "\\cdot")
+	("v" . "\\forall")
+	("s" . "\\sum_{$1}^{$2}$0")
+	("p" . "\\prod_{$1}^{$2}$0")
+	("d" . "\\partial")
+	("e" . "\\exists")
+	("i" . "\\int_{$1}^{$2}$0")
+	("c" . "\\cap")
+	("u" . "\\cup")
+	("0" . "\\emptyset")
+	("^" . "\\widehat{$1}$0")
+	("_" . "\\overline{$1}$0")
+	("~" . "\\sim")
+	("|" . "\\mid")
+	("_|" . "\\perp")))
+
+(setq my/latex-math-prefix ";")
+
+(yas-define-snippets
+ 'latex-mode
+ (mapcar
+  (lambda (elem)
+    (let ((key (car elem))
+	  (value (cdr elem)))
+      (list (concat my/latex-math-prefix key) value (concat "Math symbol " value))))
+  my/latex-math-symbols))
+
Section snippets
+

Section snippets. The code turned out to be more complicated than just writing the snippets by hand.

+
(setq my/latex-section-snippets
+      '(("ch" . "\\chapter{$1}")
+	("sec" . "\\section{$1}")
+	("ssec" . "\\subsection{$1}")
+	("sssec" . "\\subsubsection{$1}")
+	("par" . "\\paragraph{$1}}")))
+
+(setq my/latex-section-snippets
+      (mapcar
+       (lambda (elem)
+	 `(,(car elem)
+	   ,(cdr elem)
+	   ,(progn
+	      (string-match "[a-z]+" (cdr elem))
+	      (match-string 0 (cdr elem)))))
+       my/latex-section-snippets))
+
+(dolist (elem my/latex-section-snippets)
+  (let* ((key (nth 0 elem))
+	 (value (nth 1 elem))
+	 (desc (nth 2 elem))
+	 (star-index (string-match "\{\$1\}" value)))
+    (add-to-list 'my/latex-section-snippets
+		 `(,(concat key "*")
+		   ,(concat
+		     (substring value 0 star-index)
+		     "*"
+		     (substring value star-index))
+		   ,(concat desc " with *")))
+    (add-to-list 'my/latex-section-snippets
+		 `(,(concat key "l")
+		   ,(concat value "%\n\\label{sec:$2}")
+		   ,(concat desc " with label")))))
+
+(dolist (elem my/latex-section-snippets)
+  (setf (nth 1 elem) (concat (nth 1 elem) "\n$0")))
+
+(yas-define-snippets
+ 'latex-mode
+ my/latex-section-snippets)
+

Markup & natural languages

+

Markdown

+
(use-package markdown-mode
+  :straight t
+  :mode "\\.md\\'"
+  :config
+  (setq markdown-command
+	(concat
+	 "pandoc"
+	 " --from=markdown --to=html"
+	 " --standalone --mathjax --highlight-style=pygments"
+	 " --css=pandoc.css"
+	 " --quiet"
+	 ))
+  (setq markdown-live-preview-delete-export 'delete-on-export)
+  (setq markdown-asymmetric-header t)
+  (setq markdown-open-command "/home/pavel/bin/scripts/chromium-sep")
+  (add-hook 'markdown-mode-hook #'smartparens-mode)
+  (general-define-key
+   :keymaps 'markdown-mode-map
+   "M-<left>" 'markdown-promote
+   "M-<right>" 'markdown-demote))
+
+;; (use-package livedown
+;;   :straight (:host github :repo "shime/emacs-livedown")
+;;   :commands livedown-preview
+;;   :config
+;;   (setq livedown-browser "qutebrowser"))
+

Ascii Doc

+
(use-package adoc-mode
+  :straight t)
+

PlantUML

+ + + + + + + + + + + +
Guix dependency
plantuml
+
(use-package plantuml-mode
+  :straight t
+  :mode "(\\.\\(plantuml?\\|uml\\|puml\\)\\'"
+  :config
+  (setq plantuml-executable-path "/home/pavel/.guix-extra-profiles/emacs/emacs/bin/plantuml")
+  (setq plantuml-default-exec-mode 'executable)
+  (setq plantuml-indent-level 2)
+  (setq my/plantuml-indent-regexp-return "^\s*return\s+.+$")
+  (;; (add-to-list
+   ;;  'plantuml-indent-regexp-end
+   ;;  my/plantuml-indent-regexp-return)
+   )
+  (add-to-list 'auto-mode-alist '("\\.plantuml\\'" . plantuml-mode))
+  (add-to-list 'auto-mode-alist '("\\.uml\\'" . plantuml-mode))
+  (add-hook 'plantuml-mode-hook #'smartparens-mode)
+  (general-nmap
+    :keymaps 'plantuml-mode-map
+    "RET" 'plantuml-preview))
+

Subtitles

+

A major mode to work with subtitles.

+
(use-package subed
+  :straight (:host github :repo "rndusr/subed" :files ("subed/*.el")
+		   :build (:not native-compile))
+  :config
+  (general-define-key
+   :keymaps '(subed-mode-map subed-vtt-mode-map)
+   :states '(normal)
+   "gp" #'subed-mpv-toggle-pause))
+

LTeX

+

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.

+
(use-package lsp-ltex
+  :straight t
+  :after (lsp)
+  :init
+  (setq lsp-ltex-version "15.2.0")
+  (setq lsp-ltex-check-frequency "save"))
+

A function to switch the current language.

+
(defun my/ltex-lang ()
+  (interactive)
+  (setq lsp-ltex-language (completing-read
+			   "Language: "
+			   '("en-US" "ru-RU" "de-DE")))
+  (lsp-workspace-restart (lsp--read-workspace)))
+

Check whether it’s necessary to run LTeX:

+
(defun my/ltex-need-p ()
+  (let ((file-name (buffer-file-name)))
+    (cond
+     ((null file-name) nil)
+     ((string-match-p (rx "/home/pavel/" (+ alnum) ".org" eos) file-name) nil)
+     ((string-match-p (rx (literal org-directory) "/" (or "roam" "inbox-notes" "literature-notes" "journal")) file-name) t)
+     ((string-match-p (rx (literal org-directory)) file-name) nil)
+     ((string-match-p (rx (literal (expand-file-name user-emacs-directory))) file-name) nil)
+     (t t))))
+

To use it in text-mode-hook

+
(defun my/text-mode-lsp-maybe ()
+  (when (my/ltex-need-p)
+    (lsp)))
+
+(add-hook 'text-mode-hook #'my/text-mode-lsp-maybe)
+

LanguageTool

+

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.

+

References:

+ + +
(use-package langtool
+  :straight t
+  :commands (langtool-check)
+  :config
+  (setq langtool-language-tool-server-jar "/home/pavel/bin/LanguageTool-5.7/languagetool-server.jar")
+  (setq langtool-mother-tongue "ru")
+  (setq langtool-default-language "en-US"))
+
+(my-leader-def
+  :infix "L"
+  "" '(:which-key "languagetool")
+  "c" 'langtool-check
+  "s" 'langtool-server-stop
+  "d" 'langtool-check-done
+  "n" 'langtool-goto-next-error
+  "p" 'langtool-goto-previous-error
+  "l" 'langtool-correct-buffer)
+

Reverso

+

reverso.el is a package of mine that provides Emacs interface for https://reverso.net.

+
(use-package reverso
+  :straight (:host github :repo "SqrtMinusOne/reverso.el")
+  :init
+  (my-leader-def "ar" #'reverso)
+  :config
+  (setq reverso-languages '(russian english german)))
+

Lisp

+
+
+ +

Meta Lisp

+

Some packages for editing various Lisps.

+
(use-package lispy
+  :commands (lispy-mode)
+  :straight t)
+
+(use-package lispyville
+  :hook (lispy-mode . lispyville-mode)
+  :straight t)
+
+(sp-with-modes sp-lisp-modes
+  (sp-local-pair "'" nil :actions nil))
+

Emacs Lisp

+
Package Lint
+

A package that checks for the metadata in Emacs Lisp packages.

+
(use-package flycheck-package
+  :straight t
+  :after flycheck
+  :config
+  (flycheck-package-setup))
+
General settings
+
(add-hook 'emacs-lisp-mode-hook #'aggressive-indent-mode)
+;; (add-hook 'emacs-lisp-mode-hook #'smartparens-strict-mode)
+(add-hook 'emacs-lisp-mode-hook #'lispy-mode)
+
Helper functions
+

Remove all advice from function. Source: https://emacs.stackexchange.com/questions/24657/unadvise-a-function-remove-all-advice-from-it

+
(defun advice-unadvice (sym)
+  "Remove all advices from symbol SYM."
+  (interactive "aFunction symbol: ")
+  (advice-mapc (lambda (advice _props) (advice-remove sym advice)) sym))
+
IELM
+
(add-hook 'inferior-emacs-lisp-mode-hook #'smartparens-mode)
+(my-leader-def "bi" #'ielm)
+

Common lisp

+
SLIME
+
(use-package slime
+  :straight t
+  :commands (slime)
+  :config
+  (setq inferior-lisp-program "sbcl")
+  (add-hook 'slime-repl-mode 'smartparens-mode))
+
General settings
+
(add-hook 'lisp-mode-hook #'aggressive-indent-mode)
+;; (add-hook 'emacs-lisp-mode-hook #'smartparens-strict-mode)
+(add-hook 'lisp-mode-hook #'lispy-mode)
+

Clojure

+
(use-package clojure-mode
+  :straight t
+  :mode "\\.clj[sc]?\\'"
+  :config
+  ;; (add-hook 'clojure-mode-hook #'smartparens-strict-mode)
+  (add-hook 'clojure-mode-hook #'lispy-mode)
+  (add-hook 'clojure-mode-hook #'aggressive-indent-mode))
+
+(use-package cider
+  :after clojure-mode
+  :straight t)
+

Hy

+

Python requirements:

+
    +
  • hy
  • +
  • jedhy
  • +
+ +
(use-package hy-mode
+  :straight t
+  :mode "\\.hy\\'"
+  :config
+  (add-hook 'hy-mode-hook #'lispy-mode)
+  (add-hook 'hy-mode-hook #'aggressive-indent-mode))
+

Scheme

+
(use-package geiser
+  :straight t
+  :commands (geiser run-geiser)
+  :config
+  (setq geiser-default-implementation 'guile))
+
+(use-package geiser-guile
+  :straight t
+  :after geiser)
+
+(add-hook 'scheme-mode-hook #'aggressive-indent-mode)
+(add-hook 'scheme-mode-hook #'lispy-mode)
+

CLIPS

+

An honorary Lisp.

+
(use-package clips-mode
+  :straight t
+  :mode "\\.cl\\'"
+  :disabled t
+  :config
+  (add-hook 'clips-mode 'lispy-mode))
+

Python

+

ein

+

ein is a package that allows for running Jupyter notebooks in Emacs.

+
(use-package ein
+  :straight t)
+

pyright

+

For some reason it doesn’t use pipenv python executable, so here is a small workaround.

+
(setq my/pipenv-python-alist '())
+
+(defun my/get-pipenv-python ()
+  (let ((default-directory (projectile-project-root)))
+    (if (file-exists-p "Pipfile")
+	(let ((asc (assoc default-directory my/pipenv-python-alist)))
+	  (if asc
+	      (cdr asc)
+	    (let ((python-executable
+		   (string-trim (shell-command-to-string "PIPENV_IGNORE_VIRTUALENVS=1 pipenv run which python 2>/dev/null"))))
+	      (if (string-match-p ".*not found.*" python-executable)
+		  (message "Pipfile found, but not pipenv executable!")
+		(message (format "Found pipenv python: %s" python-executable))
+		(add-to-list 'my/pipenv-python-alist (cons default-directory python-executable))
+		python-executable))))
+      "python")))
+
+(use-package lsp-pyright
+  :straight t
+  :defer t
+  :if (not my/slow-ssh)
+  :hook (python-mode . (lambda ()
+			 (require 'lsp-pyright)
+			 (setq-local lsp-pyright-python-executable-cmd (my/get-pipenv-python))
+			 (lsp))))
+
+(add-hook 'python-mode-hook #'smartparens-mode)
+(add-hook 'python-mode-hook #'hs-minor-mode)
+

pipenv

+

Pipenv is a package manager for Python.

+

Automatically creates & manages virtualenvs and stores data in Pipfile and Pipfile.lock (like npm’s package.json and package-lock.json).

+
(use-package pipenv
+  :straight t
+  :hook (python-mode . pipenv-mode)
+  :if (not my/slow-ssh)
+  :init
+  (setq
+   pipenv-projectile-after-switch-function
+   #'pipenv-projectile-after-switch-extended))
+

OFF (OFF) yapf

+

yapf is a formatter for Python files.

+ + + + + + + + + + + +
Guix dependency
python-yapf
+

References:

+ + +
(use-package yapfify
+  :straight (:repo "JorisE/yapfify" :host github)
+  :disabled
+  :commands (yapfify-region
+	     yapfify-buffer
+	     yapfify-region-or-buffer
+	     yapf-mode))
+

Global config:

+
[style]
+based_on_style = facebook
+column_limit = 80
+

black

+

black is a formatter for Python files.

+ + + + + + + + + + + +
Guix dependency
python-black
+
(use-package python-black
+  :straight t
+  :commands (python-black-buffer)
+  :config
+  (setq python-black-command "black"))
+

isort

+

isort is a Python package to sort Python imports.

+ + + + + + + + + + + +
Guix dependency
python-isort
+

References:

+ + +
(use-package py-isort
+  :straight t
+  :commands (py-isort-buffer py-isort-region))
+

The following binding calls yapf & isort on the buffer

+
(my-leader-def
+  :keymaps '(python-mode-map python-ts-mode-map)
+  "rr" (lambda ()
+	 (interactive)
+	 (save-excursion
+	   (unless (and (fboundp #'org-src-edit-buffer-p) (org-src-edit-buffer-p))
+	     (py-isort-buffer))
+	   (python-black-buffer))))
+

sphinx-doc

+

A package to generate sphinx-compatible docstrings.

+
(use-package sphinx-doc
+  :straight t
+  :hook (python-mode . sphinx-doc-mode)
+  :config
+  (my-leader-def
+    :keymaps 'sphinx-doc-mode-map
+    "rd" 'sphinx-doc))
+

pytest

+

pytest is a unit testing framework for Python.

+

Once again a function to set pytest executable from pipenv.

+

References:

+ + +
(defun my/set-pipenv-pytest ()
+  (setq-local
+   python-pytest-executable
+   (concat (my/get-pipenv-python) " -m pytest")))
+
+(use-package python-pytest
+  :straight t
+  :commands (python-pytest-dispatch)
+  :init
+  (my-leader-def
+    :keymaps 'python-mode-map
+    :infix "t"
+    "t" 'python-pytest-dispatch)
+  :config
+  <<override-pytest-run>>
+  (add-hook 'python-mode-hook #'my/set-pipenv-pytest)
+  (when (derived-mode-p 'python-mode)
+    (my/set-pipenv-pytest)))
+
Fix comint buffer width
+

For some reason, the default comint output width is way too large.

+

To fix that, I’ve modified the following function in the python-pytest package.

+
(cl-defun python-pytest--run-as-comint (&key command)
+  "Run a pytest comint session for COMMAND."
+  (let* ((buffer (python-pytest--get-buffer))
+	 (process (get-buffer-process buffer)))
+    (with-current-buffer buffer
+      (when (comint-check-proc buffer)
+	(unless (or compilation-always-kill
+		    (yes-or-no-p "Kill running pytest process?"))
+	  (user-error "Aborting; pytest still running")))
+      (when process
+	(delete-process process))
+      (let ((inhibit-read-only t))
+	(erase-buffer))
+      (unless (eq major-mode 'python-pytest-mode)
+	(python-pytest-mode))
+      (compilation-forget-errors)
+      (display-buffer buffer)
+      (setq command (format "export COLUMNS=%s; %s"
+			    (- (window-width (get-buffer-window buffer)) 5)
+			    command))
+      (insert (format "cwd: %s\ncmd: %s\n\n" default-directory command))
+      (setq python-pytest--current-command command)
+      (when python-pytest-pdb-track
+	(add-hook
+	 'comint-output-filter-functions
+	 'python-pdbtrack-comint-output-filter-function
+	 nil t))
+      (run-hooks 'python-pytest-setup-hook)
+      (make-comint-in-buffer "pytest" buffer "bash" nil "-c" command)
+      (run-hooks 'python-pytest-started-hook)
+      (setq process (get-buffer-process buffer))
+      (set-process-sentinel process #'python-pytest--process-sentinel))))
+

code-cells

+

Support for text with magic comments.

+ + + + + + + + + + + + + +
Guix dependencyDisabled
python-jupytextt
+
(use-package code-cells
+  :straight t
+  :commands (code-cells-mode code-cells-convert-ipynb))
+

tensorboard

+

A function to start up TensorBoard.

+
(setq my/tensorboard-buffer "TensorBoard-out")
+
+(defun my/tensorboard ()
+  (interactive)
+  (start-process
+   "tensorboard"
+   my/tensorboard-buffer
+   "tensorboard"
+   "serve"
+   "--logdir"
+   (car (find-file-read-args "Directory: " t)))
+  (display-buffer my/tensorboard-buffer))
+

Data serialization

+

JSON

+
(use-package json-mode
+  :straight t
+  :mode "\\.json\\'"
+  :config
+  (add-hook 'json-mode #'smartparens-mode)
+  (add-hook 'json-mode #'hs-minor-mode)
+  (my/set-smartparens-indent 'json-mode))
+

CSV

+
(use-package csv-mode
+  :straight t
+  :disabled
+  :mode "\\.csv\\'")
+

YAML

+
(use-package yaml-mode
+  :straight t
+  :mode "\\.yml\\'"
+  :config
+  (add-hook 'yaml-mode-hook 'smartparens-mode)
+  (add-hook 'yaml-mode-hook 'highlight-indent-guides-mode)
+  (add-to-list 'auto-mode-alist '("\\.yml\\'" . yaml-mode)))
+

Configuration

+

.env

+
(use-package dotenv-mode
+  :straight t
+  :mode "\\.env\\..*\\'")
+

.gitignore

+

A package to quickly create .gitignore files.

+
(use-package gitignore-templates
+  :straight t
+  :commands (gitignore-templates-insert
+	     gitignore-templates-new-file))
+

Docker

+
(use-package dockerfile-mode
+  :mode "Dockerfile\\'"
+  :straight t
+  :config
+  (add-hook 'dockerfile-mode 'smartparens-mode))
+

Jenkins

+
(use-package jenkinsfile-mode
+  :straight t
+  :config
+  (add-hook 'jenkinsfile-mode-hook #'smartparens-mode)
+  (my/set-smartparens-indent 'jenkinsfile-mode))
+

crontab

+
(use-package crontab-mode
+  :straight t)
+

nginx

+
(use-package nginx-mode
+  :straight t
+  :config
+  (my/set-smartparens-indent 'nginx-mode))
+

HCL

+
(use-package hcl-mode
+  :straight t)
+

Shell

+

sh

+
(add-hook 'sh-mode-hook #'smartparens-mode)
+

fish

+
(use-package fish-mode
+  :straight t
+  :mode "\\.fish\\'"
+  :config
+ (add-hook 'fish-mode-hook #'smartparens-mode))
+

Query languages

+

SQL

+

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.

+
(setq my/sqlformatter-dialect-choice
+      '("db2" "mariadb" "mysql" "n1ql" "plsql" "postgresql" "redshift" "spark" "sql" "tsql"))
+
+(setq my/sqlformatter-dialect "postgresql")
+
+(defun my/sqlformatter-set-dialect ()
+  "Set dialect for sql-formatter"
+  (interactive)
+  (setq my/sqlformatter-dialect
+	(completing-read "Dialect: " my/sqlformatter-dialect-choice)))
+
+(reformatter-define sqlformat
+  :program (executable-find "sql-formatter")
+  :args `("-l" ,my/sqlformatter-dialect))
+
+(my-leader-def
+  :keymaps '(sql-mode-map)
+  "rr" #'sqlformat-buffer)
+

SPARQL

+
(use-package sparql-mode
+  :straight t)
+

GraphQL

+
(use-package graphql-mode
+  :straight t)
+

Documents

+

DocView

+

Don’t know about this.

+

doc-view doesn’t look great with the default doc-view-resolution of 100. 300 is fine, but then it becomes slow.

+
(defun my/doc-view-setup ()
+  (display-line-numbers-mode -1)
+  (undo-tree-mode -1))
+
+(use-package doc-view
+  :straight (:type built-in)
+  :config
+  (setq doc-view-resolution 300)
+  (add-hook 'doc-view-mode-hook #'my/doc-view-setup)
+  (general-define-key
+   :states '(normal)
+   :keymaps '(doc-view-mode-map)
+   "j" #'doc-view-next-line-or-next-page
+   "k" #'doc-view-previous-line-or-previous-page))
+

x509

+
(use-package x509-mode
+  :straight t)
+

Java

+
(use-package lsp-java
+  :straight t
+  :after (lsp)
+  :config
+  (setq lsp-java-jdt-download-url "https://download.eclipse.org/jdtls/milestones/0.57.0/jdt-language-server-0.57.0-202006172108.tar.gz"))
+
+(add-hook 'java-mode-hook #'smartparens-mode)
+;; (add-hook 'java-mode-hook #'hs-minor-mode)
+(my/set-smartparens-indent 'java-mode)
+

Go

+
(use-package go-mode
+  :straight t
+  :mode "\\.go\\'"
+  :config
+  (my/set-smartparens-indent 'go-mode)
+  (add-hook 'go-mode-hook #'smartparens-mode)
+  (add-hook 'go-mode-hook #'hs-minor-mode))
+

.NET

+

C#

+ + + + + + + + + + + + + + + + + +
Guix dependenciesDisabled
omnisharpt
dotnett
+

Disabled that for now because it depends on the old tree sitter.

+
(use-package csharp-mode
+  :straight t
+  :mode "\\.cs\\'"
+  :disabled t
+  :config
+  (setq lsp-csharp-server-path (executable-find "omnisharp-wrapper"))
+  (add-hook 'csharp-mode-hook #'csharp-tree-sitter-mode)
+  (add-hook 'csharp-tree-sitter-mode-hook #'smartparens-mode)
+  (add-hook 'csharp-mode-hook #'hs-minor-mode)
+  (my/set-smartparens-indent 'csharp-tree-sitter-mode))
+

MSBuild

+
(use-package csproj-mode
+  :straight t
+  :mode "\\.csproj\\'"
+  :config
+  (add-hook 'csproj-mode #'smartparens-mode))
+

Haskell

+
(use-package haskell-mode
+  :straight t
+  :mode "\\.hs\\'")
+
+(use-package lsp-haskell
+  :straight t
+  :after (lsp haskell-mode))
+

nix

+
(use-package nix-mode
+  :straight t
+  :mode "\\.nix\\'"
+  :config
+  (add-hook 'nix-mode-hook #'smartparens-mode)
+  (my/set-smartparens-indent 'nix-mode))
+

Lua

+
(use-package lua-mode
+  :straight t
+  :mode "\\.lua\\'"
+  :hook (lua-mode . smartparens-mode))
+
+(my/set-smartparens-indent 'lua-mode)
+

Org Mode

+

Org mode is a tool that leverages plain-text files for tasks like making notes, literate programming, task management, etc.

+

References:

+ +

Installation & basic settings

+

Use the built-in org mode (:type built-in).

+
(use-package org
+  :straight (:type built-in)
+  :if (not my/remote-server)
+  :defer t
+  :init
+  (setq org-directory (expand-file-name "~/30-39 Life/32 org-mode"))
+  (unless (file-exists-p org-directory)
+    (mkdir org-directory t))
+  :config
+  (setq org-startup-indented (not my/is-termux))
+  (setq org-return-follows-link t)
+  (setq org-src-tab-acts-natively nil)
+  (add-hook 'org-mode-hook 'smartparens-mode)
+  (add-hook 'org-agenda-mode-hook
+	    (lambda ()
+	      (visual-line-mode -1)
+	      (toggle-truncate-lines 1)
+	      (display-line-numbers-mode 0)))
+  (add-hook 'org-mode-hook
+	    (lambda ()
+	      (rainbow-delimiters-mode -1))))
+

Encryption

+

Setting up org-crypt to encrypt parts of file.

+
(with-eval-after-load-norem 'org
+  (require 'org-crypt)
+  (org-crypt-use-before-save-magic)
+  (setq org-tags-exclude-from-inheritance '("crypt"))
+  (setq org-crypt-key "C1EC867E478472439CC82410DE004F32AFA00205"))
+

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:

+
(defun my/epa--select-keys-around (fun prompt keys)
+  (if (= (seq-length keys) 1)
+      keys
+    (funcall fun prompt keys)))
+
+(with-eval-after-load-norem 'epa
+  (advice-add #'epa--select-keys :around #'my/epa--select-keys-around))
+
+(unless my/remote-server
+  (setq epa-file-encrypt-to '("DE004F32AFA00205")))
+

org-contrib

+

org-contrib is a package with various additions to Org. I use the following:

+
    +
  • ox-extra - extensions for org export
  • +
+

This used to have org-contacts and ol-notmuch at some point, but they have since been migrated to separate repos.

+
(use-package org-contrib
+  :straight (org-contrib
+	     :type git
+	     :repo "https://git.sr.ht/~bzg/org-contrib"
+	     :build t)
+  :after (org)
+  :if (not my/remote-server)
+  :config
+  (require 'ox-extra)
+  (ox-extras-activate '(latex-header-blocks ignore-headlines)))
+

ol-notmuch

+

ol-notmuch is a package that adds Org links to notmuch messages.

+
(unless (or my/remote-server my/is-termux)
+  (use-package ol-notmuch
+    :straight t
+    :after (org notmuch)))
+

org-tempo

+

org-tempo is a convinient package that provides snippets for various org blocks.

+
(with-eval-after-load 'org
+  (require 'org-tempo)
+  (add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp"))
+  (add-to-list 'org-structure-template-alist '("py" . "src python"))
+  (add-to-list 'org-structure-template-alist '("sq" . "src sql")))
+

evil-org

+

Better integration with evil-mode.

+
(use-package evil-org
+  :straight t
+  :hook (org-mode . evil-org-mode)
+  :config
+  (add-hook 'evil-org-mode-hook
+	    (lambda ()
+	      (evil-org-set-key-theme '(navigation insert textobjects additional calendar todo))))
+  (add-to-list 'evil-emacs-state-modes 'org-agenda-mode)
+  (require 'evil-org-agenda)
+  (evil-org-agenda-set-keys))
+

Support for relative URLs

+

Source: https://emacs.stackexchange.com/questions/9807/org-mode-dont-change-relative-urls

+
(defun my/export-rel-url (path desc format)
+  (cl-case format
+    (html (format "<a href=\"%s\">%s</a>" path (or desc path)))
+    (latex (format "\\href{%s}{%s}" path (or desc path)))
+    (otherwise path)))
+
+(with-eval-after-load 'org
+  (org-link-set-parameters "rel" :follow #'browse-url :export #'my/export-rel-url))
+

Keybindings & stuff

+

I’ve moved this block above because the my-leader-def expression in the next block seems to override the previous ones. So it has to be on the top.

+

General keybindings

+
(with-eval-after-load-norem 'org
+  (general-define-key
+   :keymaps 'org-mode-map
+   "C-c d" 'org-decrypt-entry
+   "C-c e" 'org-encrypt-entry
+   "M-p" 'org-latex-preview
+   "M-o" 'org-redisplay-inline-images)
+
+  (general-define-key
+   :keymaps 'org-mode-map
+   :states '(normal emacs)
+   "L" 'org-shiftright
+   "H" 'org-shiftleft
+   "S-<next>" 'org-next-visible-heading
+   "S-<prior>" 'org-previous-visible-heading
+   "M-0" 'org-next-visible-heading
+   "M-9" 'org-previous-visible-heading
+   "M-]" 'org-babel-next-src-block
+   "M-[" 'org-babel-previous-src-block)
+
+  (general-define-key
+   :keymaps 'org-agenda-mode-map
+   "M-]" 'org-agenda-later
+   "M-[" 'org-agenda-earlier)
+
+  (general-nmap :keymaps 'org-mode-map "RET" 'org-ctrl-c-ctrl-c))
+
+
(defun my/org-link-copy (&optional arg)
+  "Extract URL from org-mode link and add it to kill ring."
+  (interactive "P")
+  (let* ((link (org-element-lineage (org-element-context) '(link) t))
+	 (type (org-element-property :type link))
+	 (url (org-element-property :path link))
+	 (url (concat type ":" url)))
+    (kill-new url)
+    (message (concat "Copied URL: " url))))
+
+(with-eval-after-load-norem 'org
+  (general-nmap :keymaps 'org-mode-map
+    "C-x C-l" 'my/org-link-copy))
+
+

An idea born from discussing Org Mode navigation with @Infu.

+

Modifying org-babel-next-src-block and org-babel-previous-src-block to ignore hidden source blocks.

+
(defun my/org-babel-next-visible-src-block (arg)
+  "Move to the next visible source block.
+
+With ARG, repeats or can move backward if negative."
+  (interactive "p")
+  (let ((regexp org-babel-src-block-regexp))
+    (if (< arg 0)
+	    (beginning-of-line)
+      (end-of-line))
+    (while (and (< arg 0) (re-search-backward regexp nil :move))
+      (unless (bobp)
+	    (while (pcase (get-char-property-and-overlay (point) 'invisible)
+			 (`(outline . ,o)
+			  (goto-char (overlay-start o))
+			  (re-search-backward regexp nil :move))
+			 (_ nil))))
+      (cl-incf arg))
+    (while (and (> arg 0) (re-search-forward regexp nil t))
+      (while (pcase (get-char-property-and-overlay (point) 'invisible)
+		   (`(outline . ,o)
+			(goto-char (overlay-end o))
+			(re-search-forward regexp nil :move))
+		   (_ (end-of-line) nil)))
+      (re-search-backward regexp nil :move)
+      (cl-decf arg))
+    (if (> arg 0) (goto-char (point-max)) (beginning-of-line))))
+
+(defun my/org-babel-previous-visible-src-block (arg)
+  "Move to the prevous visible source block.
+
+With ARG, repeats or can move backward if negative."
+  (interactive "p")
+  (my/org-babel-next-visible-src-block (- arg)))
+
+(with-eval-after-load 'org
+  (general-define-key
+   :keymaps 'org-mode-map
+   :states '(normal emacs)
+   "M-]" #'my/org-babel-next-visible-src-block
+   "M-[" #'my/org-babel-previous-visible-src-block))
+

Open a file from org-directory

+

A function to open a file from org-directory, excluding a few directories like roam and journal.

+
(defun my/org-file-open ()
+  (interactive)
+  (let* ((files
+	  (thread-last
+	    '("projects" "misc")
+	    (mapcar (lambda (f)
+		      (directory-files (concat org-directory "/" f) t (rx ".org" eos))))
+	    (apply #'append)
+	    (mapcar (lambda (file)
+		      (string-replace (concat org-directory "/") "" file)))
+	    (append
+	     '("inbox.org" "contacts.org")))))
+    (find-file
+     (concat org-directory "/"
+	     (completing-read "Org file: " files)))))
+
+(with-eval-after-load 'org
+  (my-leader-def
+    "o o" 'my/org-file-open))
+

Literate programing

+

Python & Jupyter

+

Use jupyter kernels for Org Mode.

+

References:

+ + +
(use-package jupyter
+  :straight t
+  :after (org)
+  :if (not (or my/remote-server my/is-termux)))
+

Refresh kernelspecs.

+

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).

+
(defun my/jupyter-refresh-kernelspecs ()
+  "Refresh Jupyter kernelspecs"
+  (interactive)
+  (jupyter-available-kernelspecs t))
+

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.

+
(defun my/jupyter-refesh-langs ()
+  "Refresh Jupyter languages"
+  (interactive)
+  (org-babel-jupyter-aliases-from-kernelspecs t))
+

Hy

+
(use-package ob-hy
+  :after (org)
+  :if (not my/remote-server)
+  :straight t)
+

View HTML in browser

+

Open HTML in the begin_export block with xdg-open.

+
(setq my/org-view-html-tmp-dir "/tmp/org-html-preview/")
+
+(use-package f
+  :straight t)
+
+(defun my/org-view-html ()
+  (interactive)
+  (let ((elem (org-element-at-point))
+	(temp-file-path (concat my/org-view-html-tmp-dir (number-to-string (random (expt 2 32))) ".html")))
+    (cond
+     ((not (eq 'export-block (car elem)))
+      (message "Not in an export block!"))
+     ((not (string-equal (plist-get (car (cdr elem)) :type) "HTML"))
+      (message "Export block is not HTML!"))
+     (t (progn
+	  (f-mkdir my/org-view-html-tmp-dir)
+	  (f-write (plist-get (car (cdr elem)) :value) 'utf-8 temp-file-path)
+	  (start-process "org-html-preview" nil "xdg-open" temp-file-path))))))
+

PlantUML

+
(with-eval-after-load 'org
+  (setq org-plantuml-executable-path "/home/pavel/.guix-extra-profiles/emacs/emacs/bin/plantuml")
+  (setq org-plantuml-exec-mode 'plantuml)
+  (add-to-list 'org-src-lang-modes '("plantuml" . plantuml)))
+

Restclient

+

restclient.el is an Emacs package to send HTTP requests. ob-restclient provides interaction with Org Babel.

+

References:

+ + +
(use-package restclient
+  :if (not my/remote-server)
+  :straight t
+  :config
+  (general-define-key
+   :keymaps 'restclient-mode-map
+   :states '(normal visual)
+   "RET" #'restclient-http-send-current
+   "M-RET" #'restclient-http-send-current-stay-in-window
+   "y" nil
+   "M-y" #'restclient-copy-curl-command)
+  (general-define-key
+   :keymaps 'restclient-response-mode-map
+   :states '(normal visual)
+   "q" #'quit-window))
+
+(use-package ob-restclient
+  :after (org restclient)
+  :if (not my/remote-server)
+  :straight t)
+

Org Babel Setup

+

Enable languages

+
(with-eval-after-load-norem 'org
+  (org-babel-do-load-languages
+   'org-babel-load-languages
+   `((emacs-lisp . t)
+     (python . t)
+     (sql . t)
+     ;; (typescript .t)
+     (hy . t)
+     (shell . t)
+     (plantuml . t)
+     (octave . t)
+     ,@(unless my/is-termux '((jupyter . t)))
+     (sparql . t)))
+
+  (add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images))
+

Use Jupyter block instead of built-in Python.

+
(with-eval-after-load 'ob-jupyter
+  (org-babel-jupyter-override-src-block "python")
+  (org-babel-jupyter-override-src-block "hy"))
+

Turn of some minor modes in source blocks.

+
(add-hook 'org-src-mode-hook
+	  (lambda ()
+	    ;; (hs-minor-mode -1)
+	    ;; (electric-indent-local-mode -1)
+	    ;; (rainbow-delimiters-mode -1)
+	    (highlight-indent-guides-mode -1)))
+

Async code blocks evaluations. Jupyter blocks have a built-in async, so they are set as ignored.

+
(use-package ob-async
+  :straight t
+  :after (org)
+  :config
+  (setq ob-async-no-async-languages-alist '("python" "hy" "jupyter-python" "jupyter-octave" "restclient")))
+

Managing Jupyter kernels

+

Functions for managing local Jupyter kernels.

+

my/insert-jupyter-kernel inserts a path to an active Jupyter kernel to the buffer. Useful to quickly write a header like:

+
#+PROPERTY: header-args:python :session <path-to-kernel>
+

my/jupyter-connect-repl opens a emacs-jupyter REPL, connected to an active kernel. my/jupyter-qtconsole runs a standalone Jupyter QtConsole.

+

Requirements: ss

+
(setq my/jupyter-runtime-folder (expand-file-name "~/.local/share/jupyter/runtime"))
+
+(defun my/get-open-ports ()
+  (mapcar
+   #'string-to-number
+   (split-string (shell-command-to-string "ss -tulpnH | awk '{print $5}' | sed -e 's/.*://'") "\n")))
+
+(defun my/list-jupyter-kernel-files ()
+  (mapcar
+   (lambda (file) (cons (car file) (cdr (assq 'shell_port (json-read-file (car file))))))
+   (sort
+    (directory-files-and-attributes my/jupyter-runtime-folder t ".*kernel.*json$")
+    (lambda (x y) (not (time-less-p (nth 6 x) (nth 6 y)))))))
+
+(defun my/select-jupyter-kernel ()
+  (let ((ports (my/get-open-ports))
+	(files (my/list-jupyter-kernel-files)))
+    (completing-read
+     "Jupyter kernels: "
+     (seq-filter
+      (lambda (file)
+	(member (cdr file) ports))
+      files))))
+
+(defun my/insert-jupyter-kernel ()
+  "Insert a path to an active Jupyter kernel into the buffer"
+  (interactive)
+  (insert (my/select-jupyter-kernel)))
+
+(defun my/jupyter-connect-repl ()
+  "Open an emacs-jupyter REPL, connected to a Jupyter kernel"
+  (interactive)
+  (jupyter-connect-repl (my/select-jupyter-kernel) nil nil nil t))
+
+(defun my/jupyter-qtconsole ()
+  "Open Jupyter QtConsole, connected to a Jupyter kernel"
+  (interactive)
+  (start-process "jupyter-qtconsole" nil "setsid" "jupyter" "qtconsole" "--existing"
+		 (file-name-nondirectory (my/select-jupyter-kernel))))
+

I’ve also noticed that there are JSON files left in the runtime folder whenever the kernel isn’t stopped correctly. So here is a cleanup function.

+
(defun my/jupyter-cleanup-kernels ()
+  (interactive)
+  (let* ((ports (my/get-open-ports))
+	 (files (my/list-jupyter-kernel-files))
+	 (to-delete (seq-filter
+		     (lambda (file)
+		       (not (member (cdr file) ports)))
+		     files)))
+    (when (and (length> to-delete 0)
+	       (y-or-n-p (format "Delete %d files?" (length to-delete))))
+      (dolist (file to-delete)
+	(delete-file (car file))))))
+

Output post-processing

+
Do not wrap the output in emacs-jupyter
+

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.

+
(defun my/jupyter-org-scalar (value)
+  (cond
+   ((stringp value) value)
+   (t (jupyter-org-scalar value))))
+
+(define-minor-mode my/emacs-jupyter-raw-output
+  "Make emacs-jupyter do raw output")
+
+(defun my/jupyter-org-scalar-around (fun value)
+  (if my/emacs-jupyter-raw-output
+      (my/jupyter-org-scalar value)
+    (funcall fun value)))
+
+(with-eval-after-load 'jupyter
+  (advice-add 'jupyter-org-scalar :around #'my/jupyter-org-scalar-around))
+
Wrap source code output
+

A function to remove the :RESULTS: drawer from results. Once again, it’s necessary because emacs-jupyter doesn’t seem to respect :results raw.

+
(defun my/org-strip-results (data)
+  (replace-regexp-in-string ":\\(RESULTS\\|END\\):\n" "" data))
+

And an all-in-one function to:

+
    +
  • prepend #+NAME: and #+CAPTION: to the source block output. Useful if the output is an image.
  • +
  • strip the :RESULTS: drawer from the output, if necessary
  • +
  • wrap results in the src block
  • +
+

As for now, it looks sufficient to format source code outputs to get a tolerable LaTeX.

+
(defun my/org-caption-wrap (data &optional name caption attrs strip-drawer src-wrap)
+  (let* ((data-s (if (and strip-drawer (not (string-empty-p strip-drawer)))
+		     (my/org-strip-results data)
+		   data))
+	 (drawer-start (if (string-match-p "^:RESULTS:.*" data-s) 10 0)))
+    (concat
+     (substring data-s 0 drawer-start)
+     (and name (not (string-empty-p name)) (concat "#+NAME:" name "\n"))
+     (and caption (not (string-empty-p caption)) (concat "#+CAPTION:" caption "\n"))
+     (and attrs (not (string-empty-p attrs)) (concat "#+ATTR_LATEX:" attrs "\n"))
+     (if (and src-wrap (not (string-empty-p src-wrap)))
+	 (concat "#+begin_src " src-wrap "\n"
+		 (substring data-s drawer-start)
+		 (when (not (string-match-p ".*\n" data-s)) "\n")
+		 "#+end_src")
+       (substring data-s drawer-start)))))
+

To use, add the following snippet to the org file:

+
#+NAME: out_wrap
+#+begin_src emacs-lisp :var data="" caption="" name="" attrs="" strip-drawer="" src-wrap="" :tangle no :exports none
+(my/org-caption-wrap data name caption attrs strip-drawer src-wrap)
+#+end_src
+

Example usage:

+
:post out_wrap(name="fig:chart", caption="График", data=*this*)
+
Apply ANSI color codes
+

SOURCE: Apply ANSI color escape sequences for Org Babel results

+

A minor mode to apply ANSI color codes after execution.

+
(defun my/babel-ansi ()
+  (when-let ((beg (org-babel-where-is-src-block-result nil nil)))
+    (save-excursion
+      (goto-char beg)
+      (when (looking-at org-babel-result-regexp)
+	(let ((end (org-babel-result-end))
+	      (ansi-color-context-region nil))
+	  (ansi-color-apply-on-region beg end))))))
+
+(define-minor-mode org-babel-ansi-colors-mode
+  "Apply ANSI color codes to Org Babel results."
+  :global t
+  :after-hook
+  (if org-babel-ansi-colors-mode
+      (add-hook 'org-babel-after-execute-hook #'my/babel-ansi)
+    (remove-hook 'org-babel-after-execute-hook #'my/babel-ansi)))
+

Executing stuff

+

A few convinient functions and keybindings to execute things in an org buffer.

+

First, execute things above and below the point:

+
(defun my/org-babel-execute-buffer-below (&optional arg)
+  (interactive "P")
+  (org-babel-eval-wipe-error-buffer)
+  (let ((point (point)))
+    (org-save-outline-visibility t
+      (org-babel-map-executables nil
+	(when (>= (point) point)
+	  (if (memq (org-element-type (org-element-context))
+			    '(babel-call inline-babel-call))
+	      (org-babel-lob-execute-maybe)
+	    (org-babel-execute-src-block arg)))))))
+
+(defun my/org-babel-execute-buffer-above (&optional arg)
+  (interactive "P")
+  (org-babel-eval-wipe-error-buffer)
+  (let ((point (point)))
+    (org-save-outline-visibility t
+      (org-babel-map-executables nil
+	(when (<= (point) point)
+	  (if (memq (org-element-type (org-element-context))
+			    '(babel-call inline-babel-call))
+	      (org-babel-lob-execute-maybe)
+	    (org-babel-execute-src-block arg)))))))
+

Some keybindings:

+
(with-eval-after-load 'org
+  (general-define-key
+   :keymaps 'org-babel-map
+   "B" #'my/org-babel-execute-buffer-below
+   "A" #'my/org-babel-execute-buffer-above)
+
+  (my-leader-def
+    :keymaps 'org-mode-map
+    "SPC b" '(:wk "org-babel")
+    "SPC b" org-babel-map))
+

Managing a literate programming project

+

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:

+
#+PROPERTY: PRJ-DIR ..
+

A function to do the prefixing:

+
(defun my/org-prj-dir (path)
+  (expand-file-name path (org-entry-get nil "PRJ-DIR" t)))
+

Example usage is as follows:

+
:tangle (my/org-prj-dir "sqrt_data/api/__init__.py")
+

Tools

+

Various small packages.

+

Presentations

+

Doing presentations with org-present.

+
(use-package hide-mode-line
+  :straight t
+  :after (org-present))
+
+(defun my/present-next-with-latex ()
+  (interactive)
+  (org-present-next)
+  (org-latex-preview '(16)))
+
+(defun my/present-prev-with-latex ()
+  (interactive)
+  (org-present-prev)
+  (org-latex-preview '(16)))
+
+(use-package org-present
+  :straight (:host github :repo "rlister/org-present")
+  :if (not my/remote-server)
+  :commands (org-present)
+  :config
+  (general-define-key
+   :keymaps 'org-present-mode-keymap
+   "<next>" 'my/present-next-with-latex
+   "<prior>" 'my/present-prev-with-latex)
+  (setq org-present-mode-hook
+	(list (lambda ()
+		(blink-cursor-mode 0)
+		(org-present-big)
+		(org-bars-mode -1)
+		;; (org-display-inline-images)
+		(org-present-hide-cursor)
+		(org-present-read-only)
+		(display-line-numbers-mode 0)
+		(hide-mode-line-mode +1)
+		(setq-local org-format-latex-options
+			    (plist-put org-format-latex-options
+				       :scale (* org-present-text-scale my/org-latex-scale 0.5)))
+		;; (org-latex-preview '(16))
+		;; TODO ^somehow this stucks at running LaTeX^
+		(setq-local olivetti-body-width 60)
+		(olivetti-mode 1))))
+  (setq org-present-mode-quit-hook
+	(list (lambda ()
+		(blink-cursor-mode 1)
+		(org-present-small)
+		(org-bars-mode 1)
+		;; (org-remove-inline-images)
+		(org-present-show-cursor)
+		(org-present-read-write)
+		(display-line-numbers-mode 1)
+		(hide-mode-line-mode 0)
+		(setq-local org-format-latex-options (plist-put org-format-latex-options :scale my/org-latex-scale))
+		(org-latex-preview '(64))
+		(olivetti-mode -1)
+		(setq-local olivetti-body-width (default-value 'olivetti-body-width))))))
+

TOC

+

Make a TOC inside the org file.

+

References:

+ + +
(use-package org-make-toc
+  :after (org)
+  :if (not my/remote-server)
+  :commands
+  (org-make-toc
+   org-make-toc-insert
+   org-make-toc-set
+   org-make-toc-at-point)
+  :straight t)
+

Screenshots

+

A nice package to make screenshots and insert them to the Org document.

+
(use-package org-attach-screenshot
+  :commands (org-attach-screenshot)
+  :straight t)
+

Transclusion

+

A package that implements transclusions in Org Mode, i.e. rendering part of one file inside another file.

+
(use-package org-transclusion
+  :after org
+  :straight (:host github :repo "nobiot/org-transclusion")
+  :config
+  (add-to-list 'org-transclusion-extensions 'org-transclusion-indent-mode)
+  (require 'org-transclusion-indent-mode)
+  (general-define-key
+   :keymaps '(org-transclusion-map)
+   :states '(normal)
+   "RET" #'org-transclusion-open-source
+   "gr" #'org-transclusion-refresh)
+  (general-define-key
+   :keymaps '(org-mode-map)
+   :states 'normal
+   "C-c t a" #'org-transclusion-add
+   "C-c t A" #'org-transclusion-add-all
+   "C-c t t" #'org-transclusion-mode))
+

Drawing

+

This package is unbelievably good. I would have never thought it’s even possible to have this in Emacs.

+
(use-package edraw-org
+  :straight (:host github :repo "misohena/el-easydraw")
+  :if (and (not my/is-termux) (not my/remote-server))
+  :after (org)
+  :config
+  (edraw-org-setup-default))
+

Managing tables

+

I use Org to manage some small tables which I want to process further. So here is a function that saves each table to a CSV file.

+
(defun my/export-org-tables-to-csv ()
+  (interactive)
+  (org-table-map-tables
+   (lambda ()
+     (when-let
+	 (name
+	  (plist-get (cadr (org-element-at-point)) :name))
+       (org-table-export
+	(concat
+	 (file-name-directory
+	  (buffer-file-name))
+	 name ".csv")
+	"orgtbl-to-csv")))))
+

Productivity & Knowledge management

+

My ongoing effort to get a productivity setup manage something in my life in Org.

+

Initial inspirations (<2021-06-30 Wed>):

+ +

Current status of what I ended up using (<2023-12-16 Sat>):

+
    +
  • org-journal for keeping a journal
  • +
  • org-roam for a knowledge base.
  • +
  • org-agenda with org-clock for tasks
  • +
+

Org Agenda & Project Management

+

This section had seen a lot of experimentation over the last… well, years.

+
Agenda & refile files
+

All my project files live in the /projects directory, so here’s a function to set up org-agenda-files and org-refile-targets accordingly.

+

Also, my project structure is somewhat chaotic, so I have an .el file in the org directory that defines some of the refile targets.

+
(defun my/update-org-agenda ()
+  (interactive)
+  (let ((project-files
+	 (mapcar
+	  (lambda (f) (concat
+		       org-directory "/projects/"
+		       f))
+	  (seq-filter
+	   (lambda (f) (not (file-directory-p f)))
+	   (directory-files
+	    (concat org-directory "/projects"))))))
+    (setq org-agenda-files
+	  `("inbox.org"
+	    "misc/habit.org"
+	    "contacts.org"
+	    ,@project-files))
+    (setq org-refile-targets
+	  `(,@(mapcar
+	       (lambda (f) `(,f . (:tag . "refile")))
+	       project-files)
+	    ,@(mapcar
+	       (lambda (f) `(,f . (:regexp . "Tasks")))
+	       project-files)))
+    (when (file-exists-p (concat org-directory "/scripts/refile.el"))
+      (load-file (concat org-directory "/scripts/refile.el"))
+      (run-hooks 'my/org-refile-hooks))))
+
+(with-eval-after-load-norem 'org
+  (setq org-roam-directory (concat org-directory "/roam"))
+  (my/update-org-agenda))
+

Refile settings

+
(setq org-refile-use-outline-path 'file)
+(setq org-outline-path-complete-in-steps nil)
+

My day ends late sometimes. Thanks John Wigley.

+
(setq org-extend-today-until 4)
+
Capture templates
+

Settings for Org capture mode. The goal here is to have a non-disruptive process to capture various ideas.

+
(defun my/generate-inbox-note-name ()
+  (format
+   "%s/inbox-notes/%s%s.org"
+   org-directory
+   (format-time-string "%Y%m%d%H%M%S")
+   (let ((note-name (read-string "Note name: ")))
+     (if (not (string-empty-p note-name))
+	 (string-replace " " "-" (concat "-" (downcase note-name)))
+       ""))))
+
+(setq org-capture-templates
+      `(("i" "Inbox" entry  (file "inbox.org")
+	 ,(concat "* TODO %?\n"
+		  "/Entered on/ %U"))
+	("e" "email" entry (file "inbox.org")
+	 ,(concat "* TODO %:from %:subject \n"
+		  "/Entered on/ %U\n"
+		  "/Received on/ %:date-timestamp-inactive\n"
+		  "%a\n"))
+	("f" "elfeed" entry (file "inbox.org")
+	 ,(concat "* TODO %:elfeed-entry-title\n"
+		  "/Entered on/ %U\n"
+		  "%a\n"))
+	("n" "note" plain (file my/generate-inbox-note-name)
+	 ,(concat "#+TODO: PROCESSED(p)\n"
+		  "\n"
+		  "* %?\n"
+		  "/Entered on/ %U"))))
+
org-clock & org-clock-agg
+

org-clock allows for tracking time spent in Org entries. org-clock-agg is my package for creating reports from org-clock records.

+

It’s been somewhat complicated to integrate into my workflow, but I think it’s been worth it because I can now create reports for:

+
    +
  • how much time i spent on which category of tasks (education / job / …);
  • +
  • time spent per activity, particularly time spent on meetings per category;
  • +
  • time spent per project
  • +
  • +
+ +
(use-package org-clock-agg
+  :straight (:host github :repo "SqrtMinusOne/org-clock-agg")
+  :commands (org-clock-agg)
+  :init
+  (with-eval-after-load 'org
+    (my-leader-def "ol" #'org-clock-agg)))
+

The following enables org-clock persistence between Emacs sessions.

+
(with-eval-after-load 'org
+  (setq org-clock-persist 'clock)
+  (org-clock-persistence-insinuate))
+

Effort estimation. Not using this as of now.

+
(with-eval-after-load-norem 'org
+  (add-to-list
+   'org-global-properties
+   '("Effort_ALL" . "0 0:05 0:10 0:15 0:30 0:45 1:00 1:30 2:00 4:00 8:00")))
+

Log DONE time

+
(setq org-log-done 'time)
+
Custom modeline positioning
+

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.

+
(defun my/org-clock-in--fix-mode-line ()
+  (when (memq 'org-mode-line-string global-mode-string)
+    (let (new-global-mode-string
+	  appended
+	  (is-first t))
+      (dolist (item global-mode-string)
+	(cond
+	 ((or (equal item '(:eval (exwm-modeline-segment)))
+	      (equal item '(:eval (persp-mode-line))))
+	  (unless appended
+	    (when is-first
+	      (push "" new-global-mode-string))
+	    (push 'org-mode-line-string new-global-mode-string)
+	    (setq appended t))
+	  (push item new-global-mode-string))
+	 ((equal item 'org-mode-line-string))
+	 (t
+	  (push item new-global-mode-string)))
+	(setq is-first nil))
+      (unless appended
+	(push 'org-mode-line-string new-global-mode-string))
+      (setq global-mode-string (nreverse new-global-mode-string)))))
+
+(add-hook 'org-clock-in-hook #'my/org-clock-in--fix-mode-line)
+
Prompt start time for org-clock-in
+

Support prompting for start time for org-clock-in:

+
(defun my/org-clock-in-prompt-time (&optional select)
+  (interactive "P")
+  (org-clock-in
+   select
+   (encode-time
+    (org-parse-time-string
+     (org-read-date t)))))
+
+(with-eval-after-load 'org
+  (my-leader-def
+    :keymaps 'org-mode-map
+    :infix "SPC"
+    "I" #'my/org-clock-in-prompt-time))
+
Put total clocked time in properties
+

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.

+
(defun my/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 :begin element)
+	     (org-element-property :end element))))
+    (with-temp-buffer
+      (insert s)
+      (org-clock-sum)
+      org-clock-file-total-minutes)))
+

And use the function to set the total clocked time.

+
(defconst my/org-clock-total-prop :CLOCK_TOTAL)
+
+(defun my/org-clock-set-total-clocked ()
+  "Set total clocked time for heading at point."
+  (interactive)
+  (save-excursion
+    (org-back-to-heading t)
+    (org-set-property
+     (substring
+      (symbol-name my/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-package org-super-agenda
+  :straight t
+  :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-group outline-path-file "their outline paths & files"
+    :key-form
+    (org-super-agenda--when-with-marker-buffer (org-super-agenda--get-marker item)
+      ;; org-ql depends on f and s anyway
+      (s-join "/" (cons
+		   (f-filename (buffer-file-name))
+		   (org-get-outline-path))))))
+

It doesn’t look great with org-bars mode, so…

+
(defun my/org-super-agenda--make-agenda-header-around (fun name)
+  (remove-text-properties 0 (length name) '(line-prefix nil) name)
+  (remove-text-properties 0 (length name) '(wrap-prefix nil) name)
+  (funcall fun (substring-no-properties name)))
+
+(with-eval-after-load 'org-super-agenda
+  (advice-add 'org-super-agenda--make-agenda-header :around #'my/org-super-agenda--make-agenda-header-around))
+
org-ql
+

org-ql is a package to query org files.

+
(use-package org-ql
+  :after (org)
+  :if (not my/remote-server)
+  :straight t
+  :init
+  ;; See https://github.com/alphapapa/org-ql/pull/237
+  (setq org-ql-regexp-part-ts-time
+	(rx " " (repeat 1 2 digit) ":" (repeat 2 digit)
+	    (optional "-" (repeat 1 2 digit) ":" (repeat 2 digit))))
+  (my-leader-def "ov" #'org-ql-view)
+  (my-leader-def "oq" #'org-ql-search))
+
Add :multi argument to the property predicate
+

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-defpred property (property &optional value &key inherit multi)
+    "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 (keywordp property)
+		     (setf property (substring (symbol-name property) 1)))
+		   (list 'property property value
+			 :inherit (if (plist-member plist :inherit)
+				      (plist-get plist :inherit)
+				    org-use-property-inheritance)
+			 :multi (when (plist-member plist :multi)
+				  (plist-get plist :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 (unless inherit
+				 (rx-to-string `(seq bol (0+ space) ":" ,property
+						     ,@(when multi '((? "+"))) ":"
+						     (1+ space) ,value (0+ space) eol)))
+		       :query query))
+		(`(,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 (unless inherit
+				 (rx-to-string `(seq bol (0+ space) ":" ,property
+						     ,@(when multi '((? "+")))
+						     ":" (1+ space)
+						     (minimal-match (1+ not-newline)) eol)))
+		       :query query)))
+    :body
+    (pcase property
+      ('nil (user-error "Property matcher requires a PROPERTY argument"))
+      (_ (pcase value
+	   ('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.
+	    (if multi
+		(when-let (values (org-ql--value-at
+				   (point) (lambda ()
+					     ;; The default separator is space
+					     (let ((org-property-separators `((,property . "\n"))))
+					       (org-entry-get (point) property inherit)))))
+		  (seq-some (lambda (v)
+			      (string-equal value v))
+			    (split-string values "\n")))
+	      (string-equal value (org-ql--value-at
+				   (point) (lambda ()
+					     (org-entry-get (point) property inherit)))))))))))
+
Recent items
+

I just want to change the default grouping in org-ql-view-recent-items

+
(cl-defun my/org-ql-view-recent-items
+    (&key num-days (type 'ts)
+	  (files (org-agenda-files))
+	  (groups '((:auto-outline-path-file t)
+		    (:auto-todo t))))
+  "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 (->> '(ts ts-active ts-inactive clocked closed)
+				(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-exhaustive type
+		   ((or 'ts 'ts-active 'ts-inactive)
+		    `(,type :from ,(- num-days) :to 0))
+		   ((or 'clocked 'closed)
+		    `(,type :from ,(- num-days) :to 0)))))
+    (org-ql-search files query
+      :title "Recent items"
+      :sort '(todo priority date)
+      :super-groups groups)))
+
Return all TODOs
+

A view to return all TODOs in a category.

+
(defun my/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-parent map crm-local-completion-map)
+		(define-key map " " 'self-insert-command)
+		map))
+	 (ivy-prescient-sort-commands nil)
+	 (categories (completing-read-multiple
+		      "Categories: "
+		      '("TEACH" "EDU" "JOB" "LIFE" "CONFIG"))))
+    (org-ql-search (org-agenda-files)
+      `(and (todo)
+	    ,@(unless (seq-empty-p categories)
+		`((category ,@categories))))
+      :sort '(priority todo deadline)
+      :super-groups '((:auto-outline-path-file t)))))
+
Configuring views
+

Putting all the above in org-ql-views.

+
(setq org-ql-views
+      (list
+       (cons "Overview: All TODO" #'my/org-ql-all-todo)
+       (cons "Review: Stale tasks"
+	     (list :buffers-files #'org-agenda-files
+		   :query '(and (todo)
+				(not (ts :from -14)))
+		   :title "Review: Stale tasks"
+		   :sort '(todo priority date)
+		   :super-groups '((:auto-outline-path-file t))))
+       (cons "Review: Recently timestamped" #'my/org-ql-view-recent-items)
+       (cons "Review: Unlinked to meetings"
+	     (list :buffers-files #'org-agenda-files
+		   :query '(and (todo "DONE" "NO")
+				(not (property "MEETING"))
+				(ts :from -7))
+		   :super-groups '((:auto-outline-path-file t))))
+       (cons "Review: Meeting" #'my/org-ql-meeting-tasks)))
+
Custom format element
+

Changing the default org-ql-view--format-element to include effort estimation and the clocked time. I wish it were more configurable out-of-the-box.

+
(defun my/org-ql-view--format-element-override (element)
+  "Format ELEMENT for `org-ql-view'.
+
+Check `org-ql-view--format-element' for the original implementation
+and lots of comments which are too long for my Emacs config."
+  (if (not element)
+      ""
+    (setf element (org-ql-view--resolve-element-properties element))
+    (let* ((properties (cadr element))
+	   (properties (cl-loop for (key val) on properties by #'cddr
+				for symbol = (intern (cl-subseq (symbol-name key) 1))
+				unless (member symbol '(parent))
+				append (list symbol val)))
+	   (title (--> (org-ql-view--add-faces element)
+		       (org-element-property :raw-value it)
+		       (org-link-display-format it)))
+	   (todo-keyword (-some--> (org-element-property :todo-keyword element)
+			   (org-ql-view--add-todo-face it)))
+	   (tag-list (if org-use-tag-inheritance
+			 (if-let ((marker (or (org-element-property :org-hd-marker element)
+					      (org-element-property :org-marker element))))
+			     (with-current-buffer (marker-buffer marker)
+			       (org-with-wide-buffer
+				(goto-char marker)
+				(cl-loop for type in (org-ql--tags-at marker)
+					 unless (or (eq 'org-ql-nil type)
+						    (not type))
+					 append type)))
+			   (display-warning 'org-ql (format "No marker found for item: %s" title))
+			   (org-element-property :tags element))
+		       (org-element-property :tags element)))
+	   (tag-string (when tag-list
+			 (--> tag-list
+			      (s-join ":" it)
+			      (s-wrap it ":")
+			      (org-add-props it nil 'face 'org-tag))))
+	   ;;  (category (org-element-property :category element))
+	   (priority-string (-some->> (org-element-property :priority element)
+			      (char-to-string)
+			      (format "[#%s]")
+			      (org-ql-view--add-priority-face)))
+	   (clock-string (let ((effort (org-element-property :EFFORT element))
+			       (clocked (org-element-property my/org-clock-total-prop element)))
+			   (cond
+			    ((and clocked effort) (format "[%s/%s]" clocked effort))
+			    ((and clocked (not effort) (format "[%s]" clocked)))
+			    ((and (not clocked) effort) (format "[EST: %s]" effort)))))
+	   (habit-property (org-with-point-at (or (org-element-property :org-hd-marker element)
+						  (org-element-property :org-marker element))
+			     (when (org-is-habit-p)
+			       (org-habit-parse-todo))))
+	   (due-string (pcase (org-element-property :relative-due-date element)
+			 ('nil "")
+			 (string (format " %s " (org-add-props string nil 'face 'org-ql-view-due-date)))))
+	   (string (s-join " " (-non-nil (list todo-keyword priority-string title due-string clock-string tag-string)))))
+      (remove-list-of-text-properties 0 (length string) '(line-prefix) string)
+      (--> string
+	   (concat "  " it)
+	   (org-add-props it properties
+	     'org-agenda-type 'search
+	     'todo-state todo-keyword
+	     'tags tag-list
+	     'org-habit-p habit-property)))))
+
+(with-eval-after-load 'org-ql
+  (advice-add #'org-ql-view--format-element :override #'my/org-ql-view--format-element-override))
+
+

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.

+
(defun my/org-meeting--prompt ()
+  (let* ((meetings (org-ql-query
+		     :select #'element-with-markers
+		     :from (org-agenda-files)
+		     :where '(and (todo) (tags "mt") (ts-active :from today to 31))
+		     :order-by 'scheduled))
+	 (data (mapcar
+		(lambda (meeting)
+		  (let ((raw-value (org-element-property :raw-value meeting))
+			(scheduled (org-format-timestamp
+				    (org-element-property :scheduled meeting)
+				    (cdr org-time-stamp-formats))))
+		    (cons (format "%-30s %s" raw-value
+				  (propertize scheduled 'face 'org-agenda-date))
+			  meeting)))
+		meetings))
+	 (ivy-prescient-sort-commands nil))
+    (cdr
+     (assoc
+      (completing-read "Meeting: " data nil t)
+      data))))
+
+(defun my/org-meeting--format-link (meeting)
+  (format "[[file:%s::*%s][%s]]"
+	  (buffer-file-name
+	   (marker-buffer
+	    (org-element-property :org-marker meeting)))
+	  (org-element-property :raw-value meeting)
+	  (org-element-property :raw-value meeting)))
+
+(defun my/org-meeting-link (&optional arg)
+  (interactive "p")
+  (save-excursion
+    (org-back-to-heading t)
+    (let* ((meeting (my/org-meeting--prompt))
+	   (link (my/org-meeting--format-link meeting))
+	   (element (org-element-at-point-no-context)))
+      (if (or (not arg) (not (org-element-property :MEETING element)))
+	  (org-set-property "MEETING" link)
+	(let ((range (org-get-property-block
+		      (org-element-property :begin element)))
+	      (case-fold-search nil))
+	  (goto-char (cdr range))
+	  (beginning-of-line)
+	  (insert-and-inherit ":MEETING+: " link "\n")
+	  (org-indent-line))))))
+
+(defun my/org-ql-meeting-tasks (meeting)
+  (interactive (list (my/org-meeting--prompt)))
+  (org-ql-search (org-agenda-files)
+    `(property "MEETING" ,(my/org-meeting--format-link meeting)
+	       :multi t)
+    :sort '(date priority todo)
+    :buffer (format "*Meeting Tasks: %s*" (org-element-property :raw-value meeting))
+    :super-groups '((:auto-outline-path t))))
+
+(defun my/org-ql-meeting-tasks-agenda ()
+  (interactive)
+  (let ((meeting (save-window-excursion
+		   (org-agenda-switch-to)
+		   (org-back-to-heading)
+		   (org-ql--add-markers
+		    (org-element-at-point)))))
+    (my/org-ql-meeting-tasks meeting)))
+
+(with-eval-after-load 'org-agenda
+  (general-define-key
+   :keymaps 'org-agenda-mode-map
+   :states '(normal motion)
+   "gm" #'my/org-ql-meeting-tasks-agenda))
+
Tracking habits
+

Let’s see how this goes.

+

References:

+ +

org-habit-stats is a pretty nice package. Using my fork until my PR is merged.

+
(use-package org-habit-stats
+  :straight (:host github :repo "ml729/org-habit-stats")
+  :after (org)
+  :config
+  (general-define-key
+   :keymaps '(org-habit-stats-mode-map)
+   :states '(normal emacs)
+   "q" #'org-habit-stats-exit
+   "<" #'org-habit-stats-calendar-scroll-left
+   ">" #'org-habit-stats-calendar-scroll-right
+   "[" #'org-habit-stats-scroll-graph-left
+   "]" #'org-habit-stats-scroll-graph-right
+   "{" #'org-habit-stats-scroll-graph-left-big
+   "}" #'org-habit-stats-scroll-graph-right-big
+   "." #'org-habit-stats-view-next-habit
+   "," #'org-habit-stats-view-previous-habit)
+   (add-hook 'org-after-todo-state-change-hook 'org-habit-stats-update-properties))
+
Custom agendas
+

Some custom agendas to fit my workflow.

+

See this answer at Emacs StackExchange for filtering the agenda block by tag:

+
(defun my/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-matcher match))
+	   (org-get-todo-state)
+	   (org-get-tags-at)
+	   (org-reduced-level (org-current-level))))
+
+(defun my/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-p match) nil next-headline))))
+

And the agendas themselves:

+
(defun my/org-scheduled-get-time ()
+  (let ((scheduled (org-get-scheduled-time (point))))
+    (if scheduled
+	(format-time-string "%Y-%m-%d" scheduled)
+      "")))
+
+(setq org-agenda-hide-tags-regexp (rx (or "org" "refile" "proj" "habit")))
+
+(setq org-agenda-custom-commands
+      `(("p" "My outline"
+	 ((agenda "" ((org-agenda-skip-function '(my/org-agenda-skip-without-match "-habit"))))
+	  (tags-todo "inbox"
+		     ((org-agenda-overriding-header "Inbox")
+		      (org-agenda-prefix-format " %i %-12:c")
+		      (org-agenda-hide-tags-regexp ".")))
+	  (tags-todo "+waitlist+SCHEDULED<=\"<+14d>\""
+		     ((org-agenda-overriding-header "Waitlist")
+		      (org-agenda-hide-tags-regexp "waitlist")
+		      (org-agenda-prefix-format " %i %-12:c %-12(my/org-scheduled-get-time)")))
+	  (tags-todo "habit+SCHEDULED<=\"<+0d>\""
+		     ((org-agenda-overriding-header "Habits")
+		      (org-agenda-prefix-format " %i %-12:c")
+		      (org-agenda-hide-tags-regexp ".")))))))
+
Alerts
+
    +
  • 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.

+
(setq my/org-alert-notify-times '(600 60))
+

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:

+
(setq my/org-alert--alerts (make-hash-table :test #'equal))
+
+(defun my/org-alert--is-scheduled (label time)
+  "Check if LABEL is scheduled to be shown an TIME."
+  (gethash (cons label time)
+	   my/org-alert--alerts nil))
+
+(defun my/org-alert--schedule (label time)
+  "Schedule LABEL to be shown at TIME, unless it's already scheduled."
+  (unless (my/org-alert--is-scheduled label time)
+    (puthash (cons label time)
+	     (run-at-time time
+			  nil
+			  (lambda ()
+			    (alert label
+				   :title "PROXIMITY ALERT")))
+	     my/org-alert--alerts)))
+

And unschedule items that need to be unscheduled:

+
(defun my/org-alert-cleanup (&optional keys)
+  "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-loop for key in keys
+	     do (puthash key t existing-hash))
+    (cl-loop for key being the hash-keys of my/org-alert--alerts
+	     unless (gethash key existing-hash)
+	     do (progn
+		  (cancel-timer (gethash key my/org-alert--alerts))
+		  (remhash key my/org-alert--alerts)))))
+

And a function to extract the required items with org-ql-query and schedule them:

+
(defun my/org-alert--update-today-alerts ()
+  (let ((items
+	 (org-ql-query
+	   :select 'element
+	   :from (org-agenda-files)
+	   :where `(and
+		    (todo "FUTURE")
+		    (ts-active :from ,(format-time-string "%Y-%m-%d %H:%M")
+			       :to ,(format-time-string
+				     "%Y-%m-%d"
+				     (time-add
+				      (current-time)
+				      (* 60 60 24)))
+			       :with-time t))
+	   :order-by 'date))
+	scheduled-keys)
+    (cl-loop
+     for item in items
+     for scheduled = (org-timestamp-to-time (org-element-property :scheduled item))
+     do (cl-loop
+	 for before-time in my/org-alert-notify-times
+	 for label = (format "%s at %s [%s min. remaining]"
+			     (org-element-property :raw-value item)
+			     (format-time-string "%H:%M" scheduled)
+			     (number-to-string (/ before-time 60)))
+	 for time = (time-convert
+		     (+ (time-convert scheduled 'integer) (- before-time)))
+	 do (progn
+	      (my/org-alert--schedule label time)
+	      (push (cons label time) scheduled-keys))))
+    (my/org-alert-cleanup scheduled-keys)))
+

Let’s wrap it into a minor mode:

+
(setq my/org-alert--timer nil)
+
+(define-minor-mode my/org-alert-mode ()
+  :global t
+  :after-hook
+  (if my/org-alert-mode
+      (progn
+	(my/org-alert--update-today-alerts)
+	(when (timerp my/org-alert--timer)
+	  (cancel-timer my/org-alert--timer))
+	(setq my/org-alert--timer
+	      (run-at-time 600 t #'my/org-alert--update-today-alerts)))
+    (when (timerp my/org-alert--timer)
+      (cancel-timer my/org-alert--timer))
+    (my/org-alert-cleanup)))
+

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.

+
(with-eval-after-load 'org
+  (if my/emacs-started
+      (my/org-alert-mode)
+    (add-hook 'emacs-startup-hook #'my/org-alert-mode)))
+
Copying records
+

I like to add numbers to repeating events, like meetings. E.g.

+
* Job meeting 62
+SCHEDULED: <2022-11-13 16:00>
+* Job meeting 63
+SCHEDULED: <2022-11-14 16:00>
+...
+

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:

+
(defun my/org-clone-subtree-with-time-shift (n &optional shift)
+  (interactive "nNumber of clones to produce: ")
+  (unless (wholenump n) (user-error "Invalid number of replications %s" n))
+  (when (org-before-first-heading-p) (user-error "No subtree to clone"))
+  (let* ((beg (save-excursion (org-back-to-heading t) (point)))
+	     (end-of-tree (save-excursion (org-end-of-subtree t t) (point)))
+	     (shift
+	      (or shift
+		  (if (and (not (equal current-prefix-arg '(4)))
+			       (save-excursion
+				     (goto-char beg)
+				     (re-search-forward org-ts-regexp-both end-of-tree t)))
+			  (read-from-minibuffer
+			   "Date shift per clone (e.g. +1w, empty to copy unchanged): ")
+			"")))                   ;No time shift
+	     (doshift
+	      (and (org-string-nw-p shift)
+		   (or (string-match "\\`[ \t]*\\([+-]?[0-9]+\\)\\([hdwmy]\\)[ \t]*\\'"
+						 shift)
+			   (user-error "Invalid shift specification %s" shift)))))
+    (goto-char end-of-tree)
+    (unless (bolp) (insert "\n"))
+    (let* ((end (point))
+	       (template (buffer-substring beg end))
+	       (shift-n (and doshift (string-to-number (match-string 1 shift))))
+	       (shift-what (pcase (and doshift (match-string 2 shift))
+				     (`nil nil)
+				     ("h" 'hour)
+				     ("d" 'day)
+				     ("w" (setq shift-n (* 7 shift-n)) 'day)
+				     ("m" 'month)
+				     ("y" 'year)
+				     (_ (error "Unsupported time unit"))))
+	       (nmin 1)
+	       (nmax n)
+	       (n-no-remove -1)
+	       (org-id-overriding-file-name (buffer-file-name (buffer-base-buffer)))
+	       (idprop (org-entry-get beg "ID")))
+      (when (and doshift
+			 (string-match-p "<[^<>\n]+ [.+]?\\+[0-9]+[hdwmy][^<>\n]*>"
+						 template))
+	    (delete-region beg end)
+	    (setq end beg)
+	    (setq nmin 0)
+	    (setq nmax (1+ nmax))
+	    (setq n-no-remove nmax))
+      (goto-char end)
+      (cl-loop for n from nmin to nmax do
+		   (insert
+			;; Prepare clone.
+			(with-temp-buffer
+			  (insert template)
+			  (org-mode)
+			  (goto-char (point-min))
+			  (org-show-subtree)
+			  (and idprop (if org-clone-delete-id
+						  (org-entry-delete nil "ID")
+						(org-id-get-create t)))
+			  (unless (= n 0)
+			    (while (re-search-forward org-clock-line-re nil t)
+			      (delete-region (line-beginning-position)
+						     (line-beginning-position 2)))
+			    (goto-char (point-min))
+			    (while (re-search-forward org-drawer-regexp nil t)
+			      (org-remove-empty-drawer-at (point))))
+			  (goto-char (point-min))
+
+			  (when doshift
+			    (while (re-search-forward org-ts-regexp-both nil t)
+			      (org-timestamp-change (* n shift-n) shift-what))
+		    (save-excursion
+		      (goto-char (point-min))
+		      (evil-numbers/inc-at-pt n (point-min)))
+			    (unless (= n n-no-remove)
+			      (goto-char (point-min))
+			      (while (re-search-forward org-ts-regexp nil t)
+				    (save-excursion
+				      (goto-char (match-beginning 0))
+				      (when (looking-at "<[^<>\n]+\\( +[.+]?\\+[0-9]+[hdwmy]\\)")
+					(delete-region (match-beginning 1) (match-end 1)))))))
+			  (buffer-string)))))
+    (goto-char beg)))
+

My addition to that is the form with evil-numbers/inc-at-pt.

+
Keybindings
+

Global keybindings:

+
(my-leader-def
+  :infix "o"
+  "" '(:which-key "org-mode")
+  "c" 'org-capture
+  "a" 'org-agenda)
+

Local keybindings

+
(with-eval-after-load 'org
+  (my-leader-def
+    :infix "SPC"
+    :keymaps '(org-mode-map)
+    "i" #'org-clock-in
+    "o" #'org-clock-out
+    "O" #'org-clock-cancel
+    "c" #'org-clock-goto
+    "p" #'org-set-property
+    "e" #'org-set-effort
+    "r" #'org-priority
+    "m" #'my/org-meeting-link))
+

Org Journal

+

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.

+
(use-package org-journal
+  :straight t
+  :if (not my/remote-server)
+  :init
+  (my-leader-def
+    :infix "oj"
+    "" '(:which-key "org-journal")
+    "j" 'org-journal-new-entry
+    "o" 'org-journal-open-current-journal-file
+    "s" 'org-journal-tags-status)
+  :after org
+  :config
+  (setq org-journal-dir (concat org-directory "/journal"))
+  (setq org-journal-file-type 'weekly)
+  (setq org-journal-file-format "%Y-%m-%d.org")
+  (setq org-journal-date-format "%A, %Y-%m-%d")
+  (setq org-journal-enable-encryption t)
+  (setq org-journal-time-format-post-midnight "PM: %R "))
+

So, org-journal-tags is my package that implements a tagging system for org-journal.

+
(use-package org-journal-tags
+  :straight (:host github :repo "SqrtMinusOne/org-journal-tags")
+  :after (org-journal)
+  :if (not my/remote-server)
+  :config
+  (org-journal-tags-autosync-mode)
+  (general-define-key
+   :keymaps 'org-journal-mode-map
+   "C-c t" #'org-journal-tags-insert-tag))
+

Also, I want to add some extra information to the journal. Here’s a functionality to get the current weather from wttr.in:

+
(use-package request
+  :straight t
+  :defer t)
+
+(defvar my/weather-last-time 0)
+(defvar my/weather-value nil)
+
+(defun my/weather-get ()
+  (when (> (- (time-convert nil 'integer) my/weather-last-time)
+	   (* 60 5))
+    (request (format "https://wttr.in/%s" my/location)
+      :params '(("format" . "%l:%20%C%20%t%20%w%20%p"))
+      :sync t
+      :parser (lambda () (url-unhex-string (buffer-string)))
+      :timeout 10
+      :success (cl-function
+		(lambda (&key data &allow-other-keys)
+		  (setq my/weather-value data)
+		  (setq my/weather-last-time (time-convert nil 'integer))))
+      :error
+      (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
+		     (message "Got error: %S" error-thrown)))))
+  my/weather-value)
+

Let’s also try to log the current mood:

+
(defun my/get-mood ()
+  (let* ((crm-separator " ")
+	 (crm-local-completion-map
+	      (let ((map (make-sparse-keymap)))
+		(set-keymap-parent map crm-local-completion-map)
+		(define-key map " " 'self-insert-command)
+		map))
+	 (ivy-prescient-sort-commands nil))
+    (mapconcat
+     #'identity
+     (completing-read-multiple
+      "How do you feel: "
+      my/mood-list)
+     " ")))
+

And here’s the function that creates a drawer with such information. At the moment, it’s:

+
    +
  • Emacs version
  • +
  • Hostname
  • +
  • Location
  • +
  • Weather
  • +
  • Current EMMS track
  • +
  • Current mood
  • +
+ +
(defun my/set-journal-header ()
+  (org-set-property "Emacs" emacs-version)
+  (org-set-property "Hostname" system-name)
+  (org-journal-tags-prop-apply-delta :add (list (format "host.%s" (system-name))))
+  (when (boundp 'my/location)
+    (org-set-property "Location" my/location)
+    (when-let ((weather (my/weather-get)))
+      (org-set-property "Weather" weather)))
+  (when (boundp 'my/loc-tag)
+    (org-journal-tags-prop-apply-delta :add (list my/loc-tag)))
+  (when (fboundp 'emms-playlist-current-selected-track)
+    (let ((track (emms-playlist-current-selected-track)))
+      (when track
+	(let ((album (cdr (assoc 'info-album track)))
+	      (artist (or (cdr (assoc 'info-albumartist track))
+			  (cdr (assoc 'info-album track))))
+	      (title (cdr (assoc 'info-title track)))
+	      (string ""))
+	  (when artist
+	    (setq string (concat string "[" artist "] ")))
+	  (when album
+	    (setq string (concat string album " - ")))
+	  (when title
+	    (setq string (concat string title)))
+	  (when (> (length string) 0)
+	    (org-set-property "EMMS_Track" string))))))
+  (when-let (mood (my/get-mood))
+    (org-set-property "Mood" mood)))
+
+(add-hook 'org-journal-after-entry-create-hook
+	  #'my/set-journal-header)
+

Bibliography

+

I use Zotero to manage my bibliograhy.

+

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.

+
(use-package org-ref
+  :straight (:files (:defaults "citeproc" (:exclude "*helm*")))
+  :if (not my/remote-server)
+  :init
+  (setq bibtex-dialect 'biblatex)
+  (setq bibtex-completion-bibliography '("~/30-39 Life/32 org-mode/library.bib"))
+  (setq bibtex-completion-library-path '("~/30-39 Life/33 Library"))
+  (setq bibtex-completion-notes-path "~/Documents/org-mode/literature-notes")
+  (setq bibtex-completion-display-formats
+	'((t . "${author:36} ${title:*} ${note:10} ${year:4} ${=has-pdf=:1}${=type=:7}")))
+  (setq bibtex-completion-pdf-open-function
+	(lambda (file)
+	  (start-process "dired-open" nil
+			 "xdg-open" (file-truename file))))
+  :after (org)
+  :config
+  (with-eval-after-load 'ivy-bibtex
+    (require 'org-ref-ivy))
+  (general-define-key
+   :keymaps 'org-mode-map
+   "C-c l" #'org-ref-insert-link-hydra/body)
+  (general-define-key
+   :keymaps 'bibtex-mode-map
+   "M-RET" 'org-ref-bibtex-hydra/body))
+
ivy-bibtex
+

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

+
(use-package ivy-bibtex
+  :after (org-ref)
+  :straight t
+  :init
+  (my-leader-def "fB" 'ivy-bibtex))
+
+(add-hook 'bibtex-mode 'smartparens-mode)
+

Org Roam

+

org-roam is a plain-text knowledge database.

+

Things I tried with Org Roam:

+
    +
  • Managing projects. Ended up preferring plain Org.
  • +
  • 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):

+ +
Basic package configuration
+ + + + + + + + + + + + + + +
Guix dependency
emacs-emacsql-sqlite3
graphviz
+

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.

+
+

References:

+ + +
(use-package emacsql-sqlite
+  :defer t
+  :if (not my/remote-server)
+  :straight (:type built-in))
+
+(use-package org-roam
+  :straight (:host github :repo "org-roam/org-roam"
+		   :files (:defaults "extensions/*.el"))
+  :if (not my/remote-server)
+  :after org
+  :init
+  (setq org-roam-file-extensions '("org"))
+  (setq org-roam-v2-ack t)
+  (setq orb-insert-interface 'ivy-bibtex)
+  (setq org-roam-node-display-template (concat "${title:*} " (propertize "${tags:10}" 'face 'org-tag)))
+  :config
+  (org-roam-setup)
+  (require 'org-roam-protocol))
+
Capture templates
+

Capture templates for org-roam-capture. As for now, nothing too complicated here.

+
(setq org-roam-capture-templates
+      `(("d" "default" plain "%?"
+	 :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
+	 :unnarrowed t)
+	("e" "encrypted" plain "%?"
+	 :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org.gpg" "#+title: ${title}\n")
+	 :unnarrowed t)))
+
Keybindings
+

A set of keybindings to quickly access things in Org Roam.

+
(with-eval-after-load 'org-roam
+  (my-leader-def
+    :infix "or"
+    "" '(:which-key "org-roam")
+    "i" 'org-roam-node-insert
+    "r" 'org-roam-node-find
+    "g" 'org-roam-graph
+    "c" 'org-roam-capture
+    "b" 'org-roam-buffer-toggle)
+  (general-define-key
+   :keymaps 'org-roam-mode-map
+   :states '(normal)
+   "TAB" #'magit-section-toggle
+   "q" #'quit-window
+   "k" #'magit-section-backward
+   "j" #'magit-section-forward
+   "gr" #'revert-buffer
+   "RET" #'org-roam-buffer-visit-thing))
+
+(with-eval-after-load 'org
+  (my-leader-def
+    :keymap 'org-mode-map
+    :infix "or"
+    "t" 'org-roam-tag-add
+    "T" 'org-roam-tag-remove
+    "s" 'org-roam-db-autosync-mode)
+  (general-define-key
+   :keymap 'org-mode-map
+   "C-c i" 'org-roam-node-insert))
+
+

Occasionally I want to see how many backlinks a particular page has.

+

This idea came to my mind because I often write a note in the following form:

+
According to <This Person>, <some opinion>
+

And I have a note called #Personalities that looks like that:

+
Philosophers:
+- <This Person>
+- <That Person>
+- <Another Person>
+...
+

So I’m curious to see how many notes I have linked to each:

+
Philosophers:
+- <This Person> [30]
+- <That Person> [40]
+- <Another Person> [20]
+...
+

The obvious way to implement that is via overlays:

+
(defface my/org-roam-count-overlay-face
+  '((t :inherit tooltip))
+  "Face for Org Roam count overlay.")
+
+(defun my/org-roam--count-overlay-make (pos count)
+  (let* ((overlay-value (concat
+			 " "
+			 (propertize
+			  (format "%d" count)
+			  'face 'my/org-roam-count-overlay-face)
+			 " "))
+	 (ov (make-overlay pos pos (current-buffer) nil t)))
+    (overlay-put ov 'roam-backlinks-count count)
+    (overlay-put ov 'priority 1)
+    (overlay-put ov 'after-string overlay-value)))
+

Also a function to remove them:

+
(defun my/org-roam--count-overlay-remove-all ()
+  (dolist (ov (overlays-in (point-min) (point-max)))
+    (when (overlay-get ov 'roam-backlinks-count)
+      (delete-overlay ov))))
+

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:

+
(defun my/org-roam--count-overlay-make-all ()
+  (my/org-roam--count-overlay-remove-all)
+  (org-element-map (org-element-parse-buffer) 'link
+    (lambda (elem)
+      (when (string-equal (org-element-property :type elem) "id")
+	(let* ((id (org-element-property :path elem))
+	       (count (caar
+		       (org-roam-db-query
+			[:select (funcall count source)
+			 :from links
+			 :where (= dest $s1)
+			 :and (= type "id")]
+			id))))
+	  (when (< 0 count)
+	    (my/org-roam--count-overlay-make
+	     (org-element-property :end elem)
+	     count)))))))
+

And a minor mode to toggle the display in a particular org-roam buffer.

+
(define-minor-mode my/org-roam-count-overlay-mode
+  "Display backlink count for org-roam links."
+  :after-hook
+  (if my/org-roam-count-overlay-mode
+      (progn
+	(my/org-roam--count-overlay-make-all)
+	(add-hook 'after-save-hook #'my/org-roam--count-overlay-make-all nil t))
+    (my/org-roam--count-overlay-remove-all)
+    (remove-hook 'after-save-hook #'my/org-roam--count-overlay-remove-all t)))
+
Org Roam UI
+

A browser frontend to visualize the Roam database as a graph.

+

Actually, I don’t find this quite as useful as structure nodes, because over time my graph grew somewhat convoluted. But it looks impressive.

+
(use-package org-roam-ui
+  :straight (:host github :repo "org-roam/org-roam-ui" :branch "main" :files ("*.el" "out"))
+  :if (not my/remote-server)
+  :after org-roam
+  ;; :hook (org-roam . org-roam-ui-mode)
+  :init
+  (my-leader-def "oru" #'org-roam-ui-mode))
+
Deft
+

Deft is an Emacs package to quickly find notes. I use it as a full-text search engine for org-roam.

+
(use-package deft
+  :straight t
+  :if (not my/remote-server)
+  :commands (deft)
+  :after (org)
+  :init
+  (my-leader-def "ord" #'deft)
+  :config
+  (setq deft-directory org-roam-directory)
+  (setq deft-recursive t)
+  (setq deft-use-filter-string-for-filename t)
+  (add-hook 'deft-mode-hook
+	    (lambda () (display-line-numbers-mode -1)))
+  (general-define-key
+   :keymaps 'deft-mode-map
+   :states '(normal motion)
+   "q" #'quit-window
+   "r" #'deft-refresh
+   "s" #'deft-filter
+   "d" #'deft-filter-clear
+   "y" #'deft-filter-yank
+   "t" #'deft-toggle-incremental-search
+   "o" #'deft-toggle-sort-method))
+

The default deft view does not look that great because of various Roam metadata. To improve that, we can tweak deft-strip-summary-regexp:

+
(setq deft-strip-summary-regexp
+      (rx (or
+	   (: ":PROPERTIES:" (* anything) ":END:")
+	   (: "#+" (+ alnum) ":" (* nonl))
+	   (regexp "[\n\t]"))))
+

And advise deft-parse-summary to filter out Org links:

+
(defun my/deft-parse-summary-around (fun contents title)
+  (funcall fun (org-link-display-format contents) title))
+
+(with-eval-after-load 'deft
+  (advice-add #'deft-parse-summary :around #'my/deft-parse-summary-around))
+

Advise deft-parse-title to be able to extract title from the Org property:

+
(defun my/deft-parse-title (file contents)
+  (with-temp-buffer
+    (insert contents)
+    (goto-char (point-min))
+    (if (search-forward-regexp (rx (| "#+title:" "#+TITLE:")) nil t)
+	(string-trim (buffer-substring-no-properties (point) (line-end-position)))
+      file)))
+
+(defun my/deft-parse-title-around (fun file contents)
+  (or (my/deft-parse-title file contents)
+      (funcall fun file contents)))
+
+(with-eval-after-load 'deft
+  (advice-add #'deft-parse-title :around #'my/deft-parse-title-around))
+

Review workflow

+

UPD <2022-03-27 Sun>. Out of action for now

+

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.

+
(setq my/git-diff-status
+      '(("A" . added)
+	("C" . copied)
+	("D" . deleted)
+	("M" . modified)
+	("R" . renamed)
+	("T" . type-changed)
+	("U" . unmerged)))
+
+(defun my/get-files-status (rev)
+  (let ((files (shell-command-to-string (concat "git diff --name-status " rev))))
+    (mapcar
+     (lambda (file)
+       (let ((elems (split-string file "\t")))
+	 (cons
+	  (cdr (assoc (car elems) my/git-diff-status))
+	  (nth 1 elems))))
+     (split-string files "\n" t))))
+

I’ll use it to get a list of added and changed files in the Org directory since the last review. The date should be in a format YYYY-MM-DD.

+
(defun my/org-changed-files-since-date (date)
+  (let ((default-directory org-directory))
+    (my/get-files-status (format "@{%s}" date))))
+
Data from org-roam
+

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
  • +
+ +
(setq my/org-review-roam-queries
+      '((:status added
+		 :tags (:include ("org"))
+		 :title "New Project Entries")
+	(:status changed
+		 :tags (:include ("org"))
+		 :title "Changed Project Entries")
+	(:status added
+		 :tags (:exclude ("org"))
+		 :title "New Zettelkasten Entries")
+	(:status changed
+		 :tags (:exclude ("org"))
+		 :title "Changed Zettelkasten Entries")))
+

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.

+
(defun my/org-review-format-roam (changes)
+  (cl-loop for query in my/org-review-roam-queries
+	   with nodes = (org-roam-node-list)
+	   with node-tags = (mapcar #'org-roam-node-tags nodes)
+	   for include-tags = (plist-get (plist-get query :tags) :include)
+	   for exclude-tags = (plist-get (plist-get query :tags) :exclude)
+	   ;; List of nodes filtered by :tags in query
+	   for filtered-nodes =
+	   (cl-loop for node in nodes
+		    for tags in node-tags
+		    if (and
+			(or (seq-empty-p include-tags)
+			    (seq-intersection include-tags tags))
+			(or (seq-empty-p exclude-tags)
+			    (not (seq-intersection exclude-tags tags))))
+		    collect node)
+	   ;; List of changes filtered by :status in query
+	   for filtered-changes =
+	   (cl-loop for change in changes
+		    if (and (eq (car change) (plist-get query :status))
+			    (string-match-p (rx bos "roam") (cdr change)))
+		    collect (cdr change))
+	   ;; Intersection of the two filtered lists
+	   for final-nodes =
+	   (cl-loop for node in filtered-nodes
+		    for path = (file-relative-name (org-roam-node-file node)
+						   org-directory)
+		    if (member path filtered-changes)
+		    collect node)
+	   ;; If the intersction list is not empty, format it to the result
+	   if final-nodes
+	   concat (format "** %s\n" (plist-get query :title))
+	   ;; FInal list of links, sorted by title
+	   and concat (cl-loop for node in (seq-sort
+					    (lambda (node1 node2)
+					      (string-lessp
+					       (org-roam-node-title node1)
+					       (org-roam-node-title node2)))
+					    final-nodes)
+			       concat (format "- [[id:%s][%s]]\n"
+					      (org-roam-node-id node)
+					      (org-roam-node-title node)))))
+
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).

+
(setq my/org-ql-review-queries
+      `(("Waitlist" scheduled scheduled
+	 (and
+	  (done)
+	  (tags-inherited "waitlist")))
+	("Personal tasks done" closed ,nil
+	 (and
+	  (tags-inherited "personal")
+	  (todo "DONE")))
+	("Attended meetings" closed scheduled
+	 (and
+	  (tags-inherited "meeting")
+	  (todo "PASSED")))
+	("Done project tasks" closed deadline
+	 (and
+	  (todo "DONE")
+	  (ancestors
+	   (heading "Tasks"))))))
+

The query will be executed like this: (and (date-field :from rev-date) query)

+
(defun my/org-review-exec-ql (saved rev-date)
+  (let ((query `(and
+		 (,(nth 1 saved) :from ,rev-date)
+		 ,(nth 3 saved))))
+    (org-ql-query
+      :select #'element
+      :from (org-agenda-files)
+      :where query
+      :order-by (nth 2 saved))))
+

Format one element of the query result.

+
(defun my/org-review-format-element (elem)
+  (concat
+   (string-pad
+    (plist-get (cadr elem) :raw-value)
+    40)
+   (when-let (scheduled (plist-get (cadr elem) :scheduled))
+     (concat " [SCHEDULED: " (plist-get (cadr scheduled) :raw-value) "]"))
+   (when-let (deadline (plist-get (cadr elem) :deadline))
+     (concat " [DEADLINE: " (plist-get (cadr deadline) :raw-value) "]"))))
+

Execute all the saved queries and format an Org list for the capture template.

+
(defun my/org-review-format-queries (rev-date)
+  (mapconcat
+   (lambda (results)
+     (concat "** " (car results) "\n"
+	     (string-join
+	      (mapcar (lambda (r) (concat "- " r)) (cdr results))
+	      "\n")
+	     "\n"))
+   (seq-filter
+    (lambda (result)
+      (not (seq-empty-p (cdr result))))
+    (mapcar
+     (lambda (saved)
+       (cons
+	(car saved)
+	(mapcar
+	 #'my/org-review-format-element
+	 (my/org-review-exec-ql saved rev-date))))
+     my/org-ql-review-queries))
+   "\n"))
+
Capture template
+

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.

+
(setq my/org-review-directory "review")
+
+(defun my/get-last-review-date ()
+  (->
+   (substring
+    (or
+     (-max-by
+      'string-greaterp
+      (-filter
+       (lambda (f) (not (or (string-equal f ".") (string-equal f ".."))))
+       (directory-files (f-join org-roam-directory my/org-review-directory))))
+     (format-time-string
+      "%Y-%m-%d"
+      (time-subtract
+       (current-time)
+       (seconds-to-time (* 60 60 24 14)))))
+    0 10)
+   (concat "T00:00:00-00:00")
+   parse-time-string
+   encode-time
+   (time-add (seconds-to-time (* 60 60 24)))
+   ((lambda (time)
+      (format-time-string "%Y-%m-%d" time)))))
+

A template looks like this:

+
(setq my/org-review-capture-template
+      `("r" "Review" plain
+	,(string-join
+	  '("#+title: %<%Y-%m-%d>: REVIEW"
+	    "#+category: REVIEW"
+	    "#+filetags: log review"
+	    "#+STARTUP: overview"
+	    ""
+	    "Last review date: %(org-timestamp-translate (org-timestamp-from-string (format \"<%s>\" (my/get-last-review-date))))"
+	    ""
+	    "* Roam"
+	    "%(my/org-review-format-roam (my/org-changed-files-since-date (my/get-last-review-date)))"
+	    "* Agenda"
+	    "%(my/org-review-format-queries (my/get-last-review-date))"
+	    "* Thoughts"
+	    "%?")
+	  "\n")
+	:if-new (file "review/%<%Y-%m-%d>.org.gpg")))
+
+(defun my/org-roam-capture-review ()
+  (interactive)
+  (org-roam-capture- :node (org-roam-node-create)
+		     :templates `(,my/org-review-capture-template)))
+

Contacts

+

org-contacts is a package to store contacts in an org file.

+

It seems the package has been somewhat revived in the recent months. It used things like lexical-let when I first found it.

+
(use-package org-contacts
+  :straight (:type git :repo "https://repo.or.cz/org-contacts.git")
+  :if (not my/remote-server)
+  :after (org)
+  :config
+  (setq org-contacts-files (list
+			    (concat org-directory "/contacts.org"))))
+

An example contact entry can look like this:

+
* Pavel Korytov
+:PROPERTIES:
+:TYPE:     person
+:EMAIL:    thexcloud@gmail.com
+:EMAIL+:   pvkorytov@etu.ru
+:BIRTHDAY: [1998-08-14]
+:END:
+

Calendar view

+

calfw is a nice package that displays calendars in Emacs.

+
(defun my/calfw-setup-buffer ()
+  (display-line-numbers-mode -1))
+
+(use-package calfw
+  :straight t
+  :config
+  (add-hook 'cfw:calendar-mode-hook #'my/calfw-setup-buffer))
+
+(use-package calfw-org
+  :after (calfw org)
+  :straight t)
+

org-timeblock

+
(defun my/org-timeblock-conf ()
+  (display-line-numbers-mode -1))
+
+(use-package org-timeblock
+  :straight (:host github :repo "ichernyshovvv/org-timeblock")
+  :commands (org-timeblock-mode)
+  :init
+  (my-leader-def "ot" #'org-timeblock)
+  :config
+  (add-hook 'org-timeblock-mode-hook #'my/org-timeblock-conf)
+  (general-define-key
+   :keymaps '(org-timeblock-mode-map)
+   :states '(normal visual)
+   "j" #'org-timeblock-forward-block
+   "h" #'org-timeblock-backward-column
+   "l" #'org-timeblock-forward-column
+   "k" #'org-timeblock-backward-block
+   "M-[" #'org-timeblock-day-earlier
+   "M-]" #'org-timeblock-day-later
+   "H" #'org-timeblock-day-earlier
+   "L" #'org-timeblock-day-later
+   "RET" #'org-timeblock-goto
+   "t" #'org-timeblock-todo-set
+   "q" #'quit-window))
+

UI

+

LaTeX fragments

+

A function to enable LaTeX native highlighting. Not setting this as default, because it loads LaTeX stuff.

+
(defun my/enable-org-latex ()
+  (interactive)
+  (customize-set-variable 'org-highlight-latex-and-related '(native))
+  (add-hook 'org-mode-hook (lambda () (yas-activate-extra-mode 'LaTeX-mode)))
+  (sp-local-pair 'org-mode "$" "$")
+  (sp--remove-local-pair "'"))
+

Call the function before opening an org file or reopen a buffer after calling the function.

+

Scale latex fragments preview.

+
(with-eval-after-load-norem 'org
+  (setq my/org-latex-scale 1.75)
+  (setq org-format-latex-options (plist-put org-format-latex-options :scale my/org-latex-scale)))
+

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

+
(with-eval-after-load-norem 'org
+  (setq my/latex-preview-header "\\documentclass{article}
+\\usepackage[usenames]{color}
+\\usepackage{graphicx}
+\\usepackage{grffile}
+\\usepackage{longtable}
+\\usepackage{wrapfig}
+\\usepackage{rotating}
+\\usepackage[normalem]{ulem}
+\\usepackage{amsmath}
+\\usepackage{textcomp}
+\\usepackage{amssymb}
+\\usepackage{capt-of}
+\\usepackage{hyperref}
+\\pagestyle{empty}")
+
+  (setq org-preview-latex-process-alist
+	(mapcar
+	 (lambda (item)
+	   (cons
+	    (car item)
+	    (plist-put (cdr item) :latex-header my/latex-preview-header)))
+	 org-preview-latex-process-alist)))
+

Better headers

+

org-superstar-mode is a package that makes Org heading lines look a bit prettier.

+

Disabled it for now because of overlapping functionality with org-bars.

+
(use-package org-superstar
+  :straight t
+  :disabled
+  :hook (org-mode . org-superstar-mode))
+

org-bars highlights Org indentation with bars.

+
(use-package org-bars
+  :straight (:repo "tonyaldon/org-bars" :host github)
+  :if (display-graphic-p)
+  :hook (org-mode . org-bars-mode))
+

Remove the ellipsis at the end of folded headlines, as it seems unnecessary with org-bars.

+
(defun my/org-no-ellipsis-in-headlines ()
+  (remove-from-invisibility-spec '(outline . t))
+  (add-to-invisibility-spec 'outline))
+
+(with-eval-after-load 'org-bars
+  (add-hook 'org-mode-hook #'my/org-no-ellipsis-in-headlines)
+  (when (eq major-mode 'org-mode)
+    (my/org-no-ellipsis-in-headlines)))
+

Override colors

+
(my/use-colors
+ (org-block :background (my/color-value 'bg-other))
+ (org-block-begin-line :background (my/color-value 'bg-other)
+		       :foreground (my/color-value 'grey)))
+

Export

+

Hugo

+

A package for exporting Org to Hugo. That’s how I manage my sqrtminusone.xyz.

+

References:

+ + +
(use-package ox-hugo
+  :straight t
+  :if (not my/remote-server)
+  :after ox)
+

Jupyter Notebook

+
(use-package ox-ipynb
+  :straight (:host github :repo "jkitchin/ox-ipynb")
+  :if (not my/remote-server)
+  :after ox)
+

Html export

+
(use-package htmlize
+  :straight t
+  :after ox
+  :if (not my/remote-server)
+  :config
+  (setq org-html-htmlize-output-type 'css))
+

org-ref

+
(with-eval-after-load 'org-ref
+  (setq org-ref-csl-default-locale "ru-RU")
+  (setq org-ref-csl-default-style (expand-file-name
+				   (concat user-emacs-directory
+					   "gost-r-7-0-5-2008-numeric.csl"))))
+

LaTeX

+

Add a custom LaTeX template without default packages. Packages are indented to be imported with function from Import *.sty.

+
(defun my/setup-org-latex ()
+  (setq org-latex-prefer-user-labels t)
+  (setq org-latex-compiler "xelatex") ;; Probably not necessary
+  (setq org-latex-pdf-process '("latexmk -outdir=%o %f")) ;; Use latexmk
+  (setq org-latex-listings 'minted) ;; Use minted to highlight source code
+  (setq org-latex-minted-options    ;; Some minted options I like
+	'(("breaklines" "true")
+	  ("tabsize" "4")
+	  ("autogobble")
+	  ("linenos")
+	  ("numbersep" "0.5cm")
+	  ("xleftmargin" "1cm")
+	  ("frame" "single")))
+  ;; Use extarticle without the default packages
+  (add-to-list 'org-latex-classes
+	       '("org-plain-extarticle"
+		 "\\documentclass{extarticle}
+[NO-DEFAULT-PACKAGES]
+[PACKAGES]
+[EXTRA]"
+		 ("\\section{%s}" . "\\section*{%s}")
+		 ("\\subsection{%s}" . "\\subsection*{%s}")
+		 ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
+		 ("\\paragraph{%s}" . "\\paragraph*{%s}")
+		 ("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
+  (add-to-list 'org-latex-classes
+	       '("org-plain-extreport"
+		 "\\documentclass{extreport}
+[NO-DEFAULT-PACKAGES]
+[PACKAGES]
+[EXTRA]"
+		 ("\\chapter{%s}" . "\\chapter*{%s}")
+		 ("\\section{%s}" . "\\section*{%s}")
+		 ("\\subsection{%s}" . "\\subsection*{%s}")
+		 ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
+		 ("\\paragraph{%s}" . "\\paragraph*{%s}")))
+  ;; Use beamer without the default packages
+  (add-to-list 'org-latex-classes
+	       '("org-latex-beamer"
+		 "\\documentclass{beamer}
+[NO-DEFAULT-PACKAGES]
+[PACKAGES]
+[EXTRA]"
+		 ("beamer" "\\documentclass[presentation]{beamer}"
+		  ("\\section{%s}" . "\\section*{%s}")
+		  ("\\subsection{%s}" . "\\subsection*{%s}")
+		  ("\\subsubsection{%s}" . "\\subsubsection*{%s}")))))
+
+;; Make sure to eval the function when org-latex-classes list already exists
+(with-eval-after-load-norem 'ox-latex
+  (my/setup-org-latex))
+
Fix Russian dictionary
+

No idea why, but somehow the exported file uses english words if there isn’t :default key in the dictionary.

+
(with-eval-after-load 'ox
+  (setq org-export-dictionary
+	(cl-loop for item in org-export-dictionary collect
+		 (cons
+		  (car item)
+		  (cl-loop for entry in (cdr item)
+			   if (and (equal (car entry) "ru")
+				   (plist-get (cdr entry) :utf-8))
+			   collect (list "ru" :default (plist-get (cdr entry) :utf-8))
+			   else collect entry)))))
+

System configuration

+

Functions related to literate configuration.

+

Tables for Guix Dependencies

+

This section deals with using using profiles in GNU Guix.

+

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:

+
(specifications->manifest
+ '(
+   "flac"
+   "cuetools"
+   "shntool"
+   "mpd-mpc"
+   "mpd-watcher"
+   "picard"
+   "ncmpcpp"
+   "mpd"))
+

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.
  • +
+

And here is the implementation:

+
(defun my/extract-guix-dependencies (&optional category)
+  (let ((dependencies '()))
+    (org-table-map-tables
+     (lambda ()
+       (let* ((table
+	       (seq-filter
+		(lambda (q) (not (eq q 'hline)))
+		(org-table-to-lisp)))
+	      (dep-name-index
+	       (cl-position
+		nil
+		(mapcar #'substring-no-properties (nth 0 table))
+		:test (lambda (_ elem)
+			(string-match-p "[G|g]uix.*dep" elem))))
+	      (category-name-index
+	       (cl-position
+		nil
+		(mapcar #'substring-no-properties (nth 0 table))
+		:test (lambda (_ elem)
+			(string-match-p ".*[C|c]ategory.*" elem))))
+	      (disabled-name-index
+	       (cl-position
+		nil
+		(mapcar #'substring-no-properties (nth 0 table))
+		:test (lambda (_ elem)
+			(string-match-p ".*[D|d]isabled.*" elem)))))
+	 (when dep-name-index
+	   (dolist (elem (cdr table))
+	     (when
+		 (and
+		  ;; Category
+		  (or
+		   ;; Category not set and not present in the table
+		   (and
+		    (or (not category) (string-empty-p category))
+		    (not category-name-index))
+		   ;; Category is set and present in the table
+		   (and
+		    category-name-index
+		    (not (string-empty-p category))
+		    (string-match-p category (nth category-name-index elem))))
+		  ;; Not disabled
+		  (or
+		   (not disabled-name-index)
+		   (string-empty-p (nth disabled-name-index elem))))
+	       (add-to-list
+		'dependencies
+		(substring-no-properties (nth dep-name-index elem)))))))))
+    dependencies))
+

To make it work in the configuration, it is necessary to format the list so that Scheme could read it:

+
(defun my/format-guix-dependencies (&optional category)
+  (mapconcat
+   (lambda (e) (concat "\"" e "\""))
+   (my/extract-guix-dependencies category)
+   "\n"))
+

And we need an Org snippet such as this:

+
#+NAME: packages
+#+begin_src emacs-lisp :tangle no :var category=""
+(my/format-guix-dependencies category)
+#+end_src
+

Now, creating a manifest, for example, for the desktop-polybar profile is as simple as:

+
#+begin_src scheme :tangle ~/.config/guix/manifests/desktop-polybar.scm :noweb no-export
+(specifications->manifest
+ '(
+   <<packages("desktop-polybar")>>))
+#+end_src
+

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:

+
class TestClass:
+    <<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:

+
(setq my/org-config-files
+      '("/home/pavel/Emacs.org"
+	"/home/pavel/Desktop.org"
+	"/home/pavel/Console.org"
+	"/home/pavel/Guix.org"
+	"/home/pavel/Mail.org"))
+
+(add-hook 'org-mode-hook
+	  (lambda ()
+	    (when (member (buffer-file-name) my/org-config-files)
+	      (setq-local org-confirm-babel-evaluate nil))))
+

yadm hook

+

A script to run tangle from CLI.

+
(require 'org)
+
+(org-babel-do-load-languages
+ 'org-babel-load-languages
+ '((emacs-lisp . t)
+   (shell . t)))
+
+;; Do not ask to confirm evaluations
+(setq org-confirm-babel-evaluate nil)
+
+<<guix-tables>>
+
+;; A few dummy modes to avoid being prompted for comment systax
+(define-derived-mode fish-mode prog-mode "Fish"
+  (setq-local comment-start "# ")
+  (setq-local comment-start-skip "#+[\t ]*"))
+
+(define-derived-mode yaml-mode text-mode "YAML"
+  (setq-local comment-start "# ")
+  (setq-local comment-start-skip "#+ *"))
+
+(mapcar #'org-babel-tangle-file
+	'("/home/pavel/Emacs.org"
+	  "/home/pavel/Desktop.org"
+	  "/home/pavel/Console.org"
+	  "/home/pavel/Guix.org"
+	  "/home/pavel/Mail.org"))
+

To launch from CLI, run:

+
emacs -Q --batch -l run-tangle.el
+

I have added this line to yadm’s post_alt hook, so to run tangle after yadm alt

+

Regenerate desktop config

+

Somewhat similar to the previous one… Occasinally I want to re-tangle all desktop configuration files, for instance to apply a new theme.

+
(defun my/regenerate-desktop ()
+  (interactive)
+  (org-babel-tangle-file "/home/pavel/Desktop.org")
+  (org-babel-tangle-file "/home/pavel/Console.org")
+  (call-process "xrdb" nil nil nil "-load" "/home/pavel/.Xresources")
+  (call-process "~/bin/polybar.sh")
+  (call-process "pkill" nil nil nil "dunst")
+  (call-process "herd" nil nil nil "restart" "xsettingsd")
+  (when (fboundp #'my/exwm-set-alpha)
+    (if (my/light-p)
+	(my/exwm-set-alpha 100)
+      (my/exwm-set-alpha 90))))
+

Applications

+

Dired

+

Dired is the built-in Emacs file manager. It’s so good that it’s strange that, to my knowledge, no one tried to replicate it outside of Emacs.

+

I currently use it as my primary file manager.

+

Basic config & keybindings

+

My config mostly follows ranger’s and vifm’s keybindings which I’m used to.

+
(use-package dired
+  :ensure nil
+  :custom ((dired-listing-switches "-alh --group-directories-first"))
+  :commands (dired)
+  :config
+  (setq dired-dwim-target t)
+  (setq wdired-allow-to-change-permissions t)
+  (setq wdired-create-parent-directories t)
+  (setq dired-recursive-copies 'always)
+  (setq dired-recursive-deletes 'always)
+  (setq dired-kill-when-opening-new-dired-buffer t)
+  (add-hook 'dired-mode-hook
+	    (lambda ()
+	      (setq truncate-lines t)
+	      (visual-line-mode nil)))
+
+  (when my/is-termux
+    (add-hook 'dired-mode-hook #'dired-hide-details-mode))
+  (general-define-key
+   :states '(normal)
+   :keymaps 'dired-mode-map
+   "h" #'dired-up-directory
+   "l" #'dired-find-file
+   "=" #'dired-narrow
+   "-" #'my/dired-create-empty-file-subtree
+   "~" #'vterm
+   "M-r" #'wdired-change-to-wdired-mode
+   "<left>" #'dired-up-directory
+   "<right>" #'dired-find-file
+   "M-<return>" #'dired-open-xdg))
+
+(defun my/dired-home ()
+  "Open dired at $HOME"
+  (interactive)
+  (dired (expand-file-name "~")))
+
+(my-leader-def
+  "ad" #'dired)
+

Addons

+

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.

+
(use-package diredfl
+  :straight t
+  :after (dired)
+  :config
+  (diredfl-global-mode 1))
+

dired-subtree is a package that enables managing Dired buffers in a tree-like manner. By default evil-collection maps dired-subtree-toggle to TAB.

+
(use-package dired-subtree
+  :after (dired)
+  :straight t)
+
+(defun my/dired-create-empty-file-subtree ()
+  (interactive)
+  (let ((default-directory (dired-current-directory)))
+    (dired-create-empty-file
+     (read-file-name "Create empty file: "))))
+

dired-sidebar enables opening Dired in sidebar. For me, with dired-subtree this makes dired a better option than Treemacs.

+
(defun my/dired-sidebar-toggle ()
+  (interactive)
+  (if (not current-prefix-arg)
+      (dired-sidebar-toggle-sidebar)
+    (let ((dired-sidebar-follow-file-at-point-on-toggle-open
+	   current-prefix-arg)
+	  (current-prefix-arg nil))
+      (dired-sidebar-toggle-sidebar))))
+
+(use-package dired-sidebar
+  :straight t
+  :after (dired)
+  :commands (dired-sidebar-toggle-sidebar)
+  :init
+  (setq dired-sidebar-follow-file-at-point-on-toggle-open nil)
+  (general-define-key
+   :keymaps '(normal override global)
+   "C-n" `(my/dired-sidebar-toggle
+	   :wk "dired-sidebar"))
+  :config
+  (setq dired-sidebar-width 45)
+  (defun my/dired-sidebar-setup ()
+    (toggle-truncate-lines 1)
+    (display-line-numbers-mode -1)
+    (setq-local dired-subtree-use-backgrounds nil)
+    (setq-local window-size-fixed nil))
+  (general-define-key
+   :keymaps 'dired-sidebar-mode-map
+   :states '(normal emacs)
+   "l" #'dired-sidebar-find-file
+   "h" #'dired-sidebar-up-directory
+   "=" #'dired-narrow)
+  (add-hook 'dired-sidebar-mode-hook #'my/dired-sidebar-setup)
+  (advice-add #'dired-create-empty-file :after 'dired-sidebar-refresh-buffer))
+

dired-recent.el adds history to dired.

+
(use-package dired-recent
+  :straight t
+  :after dired
+  :commands (dired-recent-open)
+  :config
+  (dired-recent-mode)
+  (general-define-key
+   :keymaps 'dired-recent-mode-map
+   "C-x C-d" nil)
+  (my-leader-def
+    "aD" '(dired-recent-open :wk "dired history")))
+

Display icons for files.

+ + + + + + + + + + + + + +
NoteType
ACHTUNGThis plugin is slow as hell with TRAMP or in gnu/store
+
(use-package all-the-icons-dired
+  :straight t
+  :if (not (or my/slow-ssh (not (display-graphic-p))))
+  :hook (dired-mode . (lambda ()
+			(unless (string-match-p "/gnu/store" default-directory)
+			  (all-the-icons-dired-mode))))
+  :config)
+

Provides stuff like dired-open-xdg

+
(use-package dired-open
+  :straight t
+  :commands (dired-open-xdg))
+

dired-du is a package that shows directory sizes

+
(use-package dired-du
+  :straight t
+  :commands (dired-du-mode)
+  :config
+  (setq dired-du-size-format t))
+

vifm-like filter

+
(use-package dired-narrow
+  :straight t
+  :commands (dired-narrow)
+  :config
+  (general-define-key
+   :keymaps 'dired-narrow-map
+   [escape] 'keyboard-quit))
+

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.

+
(use-package dired-git-info
+  :straight t
+  :after dired
+  :if (not my/slow-ssh)
+  :config
+  (general-define-key
+   :keymap 'dired-mode-map
+   :states '(normal emacs)
+   ")" 'dired-git-info-mode))
+

avy-dired is my experimentation with Avy & Dired. It’s somewhat unstable.

+
(use-package avy-dired
+  :straight (:host github :repo "SqrtMinusOne/avy-dired")
+  :after (dired)
+  :init
+  (my-leader-def "aa" #'avy-dired-goto-line))
+

Subdirectories

+

Subdirectories are one of the interesting features of Dired. It allows displaying multiple folders on the same window.

+

I add my own keybindings and some extra functionality.

+
(defun my/dired-open-this-subdir ()
+  (interactive)
+  (dired (dired-current-directory)))
+
+(defun my/dired-kill-all-subdirs ()
+  (interactive)
+  (let ((dir dired-directory))
+    (kill-buffer (current-buffer))
+    (dired dir)))
+
+(with-eval-after-load 'dired
+  (general-define-key
+   :states '(normal)
+   :keymaps 'dired-mode-map
+   "s" nil
+   "ss" 'dired-maybe-insert-subdir
+   "sl" 'dired-maybe-insert-subdir
+   "sq" 'dired-kill-subdir
+   "sk" 'dired-prev-subdir
+   "sj" 'dired-next-subdir
+   "sS" 'my/dired-open-this-subdir
+   "sQ" 'my/dired-kill-all-subdirs
+   (kbd "TAB") 'dired-hide-subdir))
+

TRAMP

+

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:

+
(setq tramp-verbose 6)
+

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.

+

So, to determine if the buffer is in TRAMP:

+
(defun my/tramp-p (&optional buffer)
+  (file-remote-p
+   (buffer-local-value 'default-directory (or buffer (current-buffer)))))
+

And advice to disable a function for TRAMP-related buffers:

+
(defun my/tramp-void-if-tramp (fun &rest args)
+  (unless (my/tramp-p)
+    (apply fun args)))
+
+(defun my/tramp-void-if-file-is-tramp (fun &optional dir)
+  (unless (file-remote-p (or dir default-directory))
+    (funcall fun dir)))
+

editorconfig. This lovely package looks for .editorconfig in the file tree.

+
(with-eval-after-load 'editorconfig
+  (advice-add #'editorconfig-apply :around #'my/tramp-void-if-tramp)
+  (advice-add #'editorconfig--disabled-for-filename
+	      :around #'my/tramp-void-if-file-is-tramp))
+

all-the-icons-dired runs test on every file in the directory.

+
(with-eval-after-load 'all-the-icons-dired
+  (advice-add #'all-the-icons-dired-mode :around #'my/tramp-void-if-tramp))
+

projectile looks for .git, .svn, etc. to find the project root. Maybe I’ll make a more economic implementation if I need one.

+
(with-eval-after-load 'projectile
+  (advice-add #'projectile-project-root :around #'my/tramp-void-if-file-is-tramp))
+

lsp does a whole lot of stuff. It probably can be used with TRAMP on faster connections, but not in my case.

+
(with-eval-after-load 'lsp
+  (advice-add #'lsp :around #'my/tramp-void-if-tramp)
+  (advice-add #'lsp-deferred :around #'my/tramp-void-if-tramp))
+

git-gutter runs git a lot of times.

+
(with-eval-after-load 'git-gutter
+  (advice-add #'git-gutter--turn-on :around #'my/tramp-void-if-tramp))
+

At any rate, it’s usable, although not perfect. +Some other optimization settings:

+
(setq remote-file-name-inhibit-cache nil)
+(setq vc-ignore-dir-regexp
+      (format "\\(%s\\)\\|\\(%s\\)"
+	      vc-ignore-dir-regexp
+	      tramp-file-name-regexp))
+

Set the default shell to bin/bash for TRAMP or on a remote server.

+
(when (or my/remote-server my/slow-ssh)
+  (setq explicit-shell-file-name "/bin/bash"))
+

Also, here is a hack to make TRAMP find ls on Guix:

+
(with-eval-after-load 'tramp
+  (setq tramp-remote-path
+	(append tramp-remote-path
+		'(tramp-own-remote-path))))
+

Bookmarks

+

A simple bookmark list for Dired, mainly to use with TRAMP. I may look into a proper bookmarking system later.

+

Bookmarks are listed in the private.el file, which has an expression like this:

+
(setq my/dired-bookmarks
+      '(("sudo" . "/sudo::/")))
+

The file itself is encrypted with yadm.

+
(defun my/dired-bookmark-open ()
+  (interactive)
+  (let ((bookmarks
+	 (mapcar
+	  (lambda (el) (cons (format "%-30s %s" (car el) (cdr el)) (cdr el)))
+	  my/dired-bookmarks)))
+    (dired
+     (cdr
+      (assoc
+       (completing-read "Dired: " bookmarks nil nil "^")
+       bookmarks)))))
+

Integrations

+

A few functions to send files from Dired to various places.

+

First, a function to get the target buffer.

+
(defun my/get-good-buffer (buffer-major-mode prompt)
+  (or
+   (cl-loop
+    for buf being the buffers
+    if (eq (buffer-local-value 'major-mode buf) buffer-major-mode)
+    collect buf into all-buffers
+    if (and (eq (buffer-local-value 'major-mode buf) buffer-major-mode)
+	    (get-buffer-window buf t))
+    collect buf into visible-buffers
+    finally return (if (= (length visible-buffers) 1)
+		       (car visible-buffers)
+		     (if (= (length all-buffers) 1)
+			 (car all-buffers)
+		       (when-let ((buffers-by-name (mapcar (lambda (b)
+							     (cons (buffer-name b) b))
+							   all-buffers)))
+			 (cdr
+			  (assoc
+			   (completing-read prompt buffers-by-name nil t)
+			   buffers-by-name))))))
+   (user-error "No buffer found!")))
+

Attach file to telega.

+
(defun my/dired-attach-to-telega (files telega-buffer)
+  (interactive
+   (list (dired-get-marked-files nil nil #'dired-nondirectory-p)
+	 (my/get-good-buffer 'telega-chat-mode "Telega buffer: ")))
+  (unless files
+    (user-error "No (non-directory) files selected"))
+  (with-current-buffer telega-buffer
+    (dolist (file files)
+      (telega-chatbuf-attach-file file))))
+

Save a file to a dired buffer.

+
(defun my/telega-save-to-dired (msg arg)
+  (interactive
+   (list (telega-msg-for-interactive)
+	 (prefix-numeric-value current-prefix-arg)))
+  (if (eq arg 4)
+      (let ((default-directory
+	     (with-current-buffer (my/get-good-buffer 'dired-mode "Dired buffer: ")
+	       (dired-current-directory))))
+	(telega-msg-save msg))
+    (telega-msg-save msg)))
+

Attach files to notmuch.

+
(defun my/dired-attach-to-notmuch (files notmuch-buffer)
+  (interactive
+   (list (dired-get-marked-files nil nil #'dired-nondirectory-p)
+	 (my/get-good-buffer 'notmuch-message-mode "Notmuch message buffer: ")))
+  (unless files
+    (user-error "No (non-directory) files selected"))
+  (with-current-buffer notmuch-buffer
+    (goto-char (point-max))
+    (dolist (file files)
+      (let ((type
+	     (or (mm-default-file-type file)
+			 "application/octet-stream")))
+	(mml-attach-file
+	 file
+	 type
+	 (mml-minibuffer-read-description)
+	 (mml-minibuffer-read-disposition type nil file))))))
+

Attach files to ement.

+
(defun my/dired-attach-to-ement (files ement-buffer)
+  (interactive
+   (list (dired-get-marked-files nil nil #'dired-nondirectory-p)
+	 (my/get-good-buffer 'ement-room-mode "Ement room buffer: ")))
+  (unless files
+    (user-error "No (non-directory) files selected"))
+  (with-current-buffer ement-buffer
+    (ement-with-room-and-session
+      (dolist (file files)
+	(ement-room-send-file
+	 file
+	 (read-from-minibuffer (format "Message body for %s: " file))
+	 ement-room
+	 ement-session)))))
+

Attach files to mastodon.

+
(defun my/dired-attach-to-mastodon (files mastodon-buffer)
+  (interactive
+   (list (dired-get-marked-files nil nil #'dired-nondirectory-p)
+	 (or (cl-loop for buf being the buffers
+		      if (eq (buffer-local-value 'mastodon-toot-mode buf) t)
+		      return buf)
+	     (user-error "No buffer found!"))))
+  (unless files
+    (user-error "No (non-directory) files selected"))
+  (with-current-buffer mastodon-buffer
+    (dolist (file files)
+      (mastodon-toot--attach-media
+       file
+       (read-from-minibuffer (format "Description for %s: " file))))))
+

And the keybindings:

+
(with-eval-after-load 'dired
+  (general-define-key
+   :states '(normal)
+   :keymaps 'dired-mode-map
+   "a" nil
+   "at" #'my/dired-attach-to-telega
+   "am" #'my/dired-attach-to-notmuch
+   "ai" #'my/dired-attach-to-ement
+   "an" #'my/dired-attach-to-mastodon))
+
(with-eval-after-load 'telega
+  (general-define-key
+   :keymaps 'telega-msg-button-map
+   "S" #'my/telega-save-to-dired))
+

Shells / Terminals

+

vterm

+

My terminal emulator of choice.

+

References:

+ +
Configuration
+

On Guix it makes more sense to use the Guix package to avoid building the vterm module, but obviously not an option on termux, hence this:

+
(when my/is-termux
+  (straight-use-package 'vterm))
+

The actual config:

+
(use-package vterm
+  :commands (vterm vterm-other-window)
+  :config
+  (setq vterm-kill-buffer-on-exit t)
+
+  (add-hook 'vterm-mode-hook
+	    (lambda ()
+	      (setq-local global-display-line-numbers-mode nil)
+	      (display-line-numbers-mode 0)))
+
+
+  (advice-add 'evil-collection-vterm-insert
+	      :before (lambda (&rest args)
+			(ignore-errors
+			  (apply #'vterm-reset-cursor-point args))))
+
+  (general-define-key
+   :keymaps 'vterm-mode-map
+   "M-q" 'vterm-send-escape
+
+   "C-h" 'evil-window-left
+   "C-l" 'evil-window-right
+   "C-k" 'evil-window-up
+   "C-j" 'evil-window-down
+
+   "C-<right>" 'evil-window-right
+   "C-<left>" 'evil-window-left
+   "C-<up>" 'evil-window-up
+   "C-<down>" 'evil-window-down
+
+   "M-<left>" 'vterm-send-left
+   "M-<right>" 'vterm-send-right
+   "M-<up>" 'vterm-send-up
+   "M-<down>" 'vterm-send-down)
+
+  (general-define-key
+   :keymaps 'vterm-mode-map
+   :states '(normal insert)
+   "<home>" 'vterm-beginning-of-line
+   "<end>" 'vterm-end-of-line)
+
+  (general-define-key
+   :keymaps 'vterm-mode-map
+   :states '(insert)
+   "C-r" 'vterm-send-C-r
+   "C-k" 'vterm-send-C-k
+   "C-j" 'vterm-send-C-j
+   "M-l" 'vterm-send-right
+   "M-h" 'vterm-send-left
+   "M-k" 'vterm-send-up
+   "M-j" 'vterm-send-down))
+
Subterminal
+

Open a terminal in the lower third of the frame with the ` key.

+

I guess that’s the first Emacs function I wrote!

+
(add-to-list 'display-buffer-alist
+	     `(,"vterm-subterminal.*"
+	       (display-buffer-reuse-window
+		display-buffer-in-side-window)
+	       (side . bottom)
+	       (reusable-frames . visible)
+	       (window-height . 0.33)))
+
+(defun my/toggle-vterm-subteminal ()
+  "Toogle subteminal."
+  (interactive)
+  (let ((vterm-window
+	(seq-find
+	 (lambda (window)
+	   (string-match
+	    "vterm-subterminal.*"
+	    (buffer-name (window-buffer window))))
+	 (window-list))))
+    (if vterm-window
+	(if (eq (get-buffer-window (current-buffer)) vterm-window)
+	    (kill-buffer (current-buffer))
+	  (select-window vterm-window))
+      (vterm-other-window "vterm-subterminal"))))
+
+(unless my/slow-ssh
+  (general-nmap "`" 'my/toggle-vterm-subteminal)
+  (general-nmap "~" 'vterm))
+
Dired integration
+

A function to get pwd for vterm. Couldn’t find a built-in function for some reason, but this seems work fine:

+
(defun my/vterm-get-pwd ()
+  (if vterm--process
+      (file-truename (format "/proc/%d/cwd" (process-id vterm--process)))
+    default-directory))
+

Now we can open dired for vterm pwd:

+
(defun my/vterm-dired-other-window ()
+  "Open dired in vterm pwd in other window"
+  (interactive)
+  (dired-other-window (my/vterm-get-pwd)))
+
+(defun my/vterm-dired-replace ()
+  "Replace vterm with dired"
+  (interactive)
+  (let ((pwd (my/vterm-get-pwd)))
+    (kill-process vterm--process)
+    (dired pwd)))
+

The second function is particularly handy because that way I can alternate between vterm and dired.

+

Keybindings:

+
(with-eval-after-load 'vterm
+  (general-define-key
+   :keymaps 'vterm-mode-map
+   :states '(normal)
+   "gd" #'my/vterm-dired-other-window
+   "gD" #'my/vterm-dired-replace))
+
With-editor integration
+

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.

+
(use-package with-editor
+  :straight t
+  :after (vterm)
+  :config
+  (add-hook 'vterm-mode-hook 'with-editor-export-editor))
+

eshell

+

A shell written in Emacs lisp. I don’t use it as of now, but keep the config just in case.

+
(defun my/configure-eshell ()
+  (add-hook 'eshell-pre-command-hook 'eshell-save-some-history)
+  (add-to-list 'eshell-output-filter-functions 'eshell-truncate-buffer)
+  (setq eshell-history-size 10000)
+  (setq eshell-hist-ingnoredups t)
+  (setq eshell-buffer-maximum-lines 10000)
+
+  (evil-define-key '(normal insert visual) eshell-mode-map (kbd "<home>") 'eshell-bol)
+  (evil-define-key '(normal insert visual) eshell-mode-map (kbd "C-r") 'counsel-esh-history)
+  (general-define-key
+   :states '(normal)
+   :keymaps 'eshell-mode-map
+   (kbd "C-h") 'evil-window-left
+   (kbd "C-l") 'evil-window-right
+   (kbd "C-k") 'evil-window-up
+   (kbd "C-j") 'evil-window-down))
+
+(use-package eshell
+  :ensure nil
+  :after evil-collection
+  :commands (eshell)
+  :init
+  (my/use-colors
+   (epe-pipeline-delimiter-face :foreground (my/color-value 'green))
+   (epe-pipeline-host-face      :foreground (my/color-value 'blue))
+   (epe-pipeline-time-face      :foreground (my/color-value 'yellow))
+   (epe-pipeline-user-face      :foreground (my/color-value 'red)))
+  :config
+  (add-hook 'eshell-first-time-mode-hook 'my/configure-eshell 90)
+  (when my/slow-ssh
+    (add-hook 'eshell-mode-hook
+	      (lambda ()
+		(setq-local company-idle-delay 1000))))
+  (setq eshell-banner-message ""))
+
+(use-package aweshell
+  :straight (:repo "manateelazycat/aweshell" :host github)
+  :after eshell
+  :init
+  (my/use-colors
+   (aweshell-alert-buffer-face  :background (color-darken-name (my/color-value 'bg) 3))
+   (aweshell-alert-command-face :foreground (my/color-value 'red) :weight 'bold))
+  :config
+  (setq eshell-prompt-regexp "^[^#\nλ]* λ[#]* ")
+  (setq eshell-highlight-prompt t)
+  (setq eshell-prompt-function 'epe-theme-pipeline))
+
+(use-package eshell-info-banner
+  :defer t
+  :if (not my/slow-ssh)
+  :straight (eshell-info-banner :type git
+				:host github
+				:repo "phundrak/eshell-info-banner.el")
+  :hook (eshell-banner-load . eshell-info-banner-update-banner)
+  :config
+  (setq eshell-info-banner-filter-duplicate-partitions t)
+  (setq eshell-info-banner-exclude-partitions '("b/efi")))
+
+(when (or my/slow-ssh my/remote-server)
+  (general-nmap "`" 'aweshell-dedicated-toggle)
+  (general-nmap "~" 'eshell))
+

shell

+

Interactive subshell (M-x shell) is a way to run commands with input and output through an Emacs buffer.

+
(defun my/setup-shell ()
+  (setq-local comint-use-prompt-regexp t)
+  (setq-local comint-prompt-read-only t))
+
+(add-hook 'shell-mode-hook #'my/setup-shell)
+

Managing dotfiles

+

A bunch of functions for managing dotfiles with yadm.

+

Open Emacs config

+
(general-define-key
+ ;; "C-c c" (my/command-in-persp "Emacs.org" "conf" 1 (find-file "~/Emacs.org"))
+ "C-c c" `(,(lambda () (interactive) (find-file "~/Emacs.org")) :wk "Emacs.org"))
+
+(my-leader-def
+  :infix "c"
+  "" '(:which-key "configuration")
+  ;; "c" (my/command-in-persp "Emacs.org" "conf" 1 (find-file "~/Emacs.org"))
+  "c" `(,(lambda () (interactive) (find-file "~/Emacs.org")) :wk "Emacs.org"))
+

Open Magit for yadm

+

Idea:

+ + +
(with-eval-after-load 'tramp
+  (add-to-list 'tramp-methods
+	       `("yadm"
+		 (tramp-login-program "yadm")
+		 (tramp-login-args (("enter")))
+		 (tramp-login-env (("SHELL") "/bin/sh"))
+		 (tramp-remote-shell "/bin/sh")
+		 (tramp-remote-shell-args ("-c")))))
+
+
+(defun my/yadm-magit ()
+  (interactive)
+  (magit-status "/yadm::"))
+
+(my-leader-def "cm" 'my/yadm-magit)
+

Open a dotfile

+

Open a file managed by yadm.

+
(defun my/open-yadm-file ()
+  "Open a file managed by yadm"
+  (interactive)
+  (find-file
+   (concat
+    (file-name-as-directory (getenv "HOME"))
+    (completing-read
+     "yadm files: "
+     (split-string
+      (shell-command-to-string "yadm ls-files $HOME --full-name") "\n")))))
+
+(general-define-key
+ "C-c f" '(my/open-yadm-file :wk "yadm file"))
+
+(my-leader-def
+  "cf" '(my/open-yadm-file :wk "yadm file"))
+

Elfeed

+

elfeed is one of the most popular Emacs packages, and it’s also one in which I ended up investing a lot of effort.

+

There’s a lot of stuff in this section, so it’s here and not in “Internet and Multimedia”.

+

General settings

+

The advice there sets shr-use-fonts to nil while rendering HTML, so the elfeed-show buffer will use monospace font.

+

Using my own fork until the modifications are merged into master.

+
(use-package elfeed
+  :straight (:repo "SqrtMinusOne/elfeed" :host github)
+  :if (not (or my/is-termux my/remote-server))
+  :commands (elfeed)
+  :init
+  (my-leader-def "ae" #'elfeed-summary)
+  (my/persp-add-rule
+    elfeed-summary-mode 0 "elfeed"
+    elfeed-search-mode 0 "elfeed"
+    elfeed-show-mode 0 "elfeed")
+  (setq shr-max-image-proportion 0.5)
+  :config
+  (setq elfeed-db-directory "~/.elfeed")
+  (setq elfeed-enclosure-default-dir (expand-file-name "~/Downloads"))
+  ;; (advice-add #'elfeed-insert-html
+  ;;             :around
+  ;;             (lambda (fun &rest r)
+  ;;               (let ((shr-use-fonts nil))
+  ;;                 (apply fun r))))
+  (general-define-key
+   :states '(normal)
+   :keymaps 'elfeed-search-mode-map
+   "o" #'my/elfeed-search-filter-source
+   "c" #'elfeed-search-clear-filter
+   "gl" (lambda () (interactive) (elfeed-search-set-filter "+later")))
+  (general-define-key
+   :states '(normal)
+   :keymaps 'elfeed-show-mode-map
+   "ge" #'my/elfeed-show-visit-eww))
+

elfeed-org allows configuring Elfeed feeds with an Org file.

+
(use-package elfeed-org
+  :straight t
+  :after (elfeed)
+  :config
+  (setq rmh-elfeed-org-files '("~/.emacs.d/private.org"))
+  (elfeed-org))
+

Some additions

+

Filter elfeed search buffer by the feed under the cursor.

+
(defun my/elfeed-search-filter-source (entry)
+  "Filter elfeed search buffer by the feed under cursor."
+  (interactive (list (elfeed-search-selected :ignore-region)))
+  (when (elfeed-entry-p entry)
+    (elfeed-search-set-filter
+     (concat
+      "@6-months-ago "
+      "+unread "
+      "="
+      (replace-regexp-in-string
+       (rx "?" (* not-newline) eos)
+       ""
+       (elfeed-feed-url (elfeed-entry-feed entry)))))))
+

Open a URL with eww.

+
(defun my/elfeed-show-visit-eww ()
+  "Visit the current entry in eww"
+  (interactive)
+  (let ((link (elfeed-entry-link elfeed-show-entry)))
+    (when link
+      (eww link))))
+

Custom faces

+

Setting up custom faces for certain tags to make the feed look a bit nicer.

+
(defface elfeed-videos-entry nil
+  "Face for the elfeed entries with tag \"videos\"")
+
+(defface elfeed-twitter-entry nil
+  "Face for the elfeed entries with tah \"twitter\"")
+
+(defface elfeed-emacs-entry nil
+  "Face for the elfeed entries with tah \"emacs\"")
+
+(defface elfeed-music-entry nil
+  "Face for the elfeed entries with tah \"music\"")
+
+(defface elfeed-podcasts-entry nil
+  "Face for the elfeed entries with tag \"podcasts\"")
+
+(defface elfeed-blogs-entry nil
+  "Face for the elfeed entries with tag \"blogs\"")
+
+(defface elfeed-govt-entry nil
+  "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
+  (setq elfeed-search-face-alist
+	'((podcasts elfeed-podcasts-entry)
+	  (music elfeed-music-entry)
+	  (gov elfeed-govt-entry)
+	  (twitter elfeed-twitter-entry)
+	  (videos elfeed-videos-entry)
+	  (emacs elfeed-emacs-entry)
+	  (blogs elfeed-blogs-entry)
+	  (unread elfeed-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.

+
(use-package elfeed-summary
+  :commands (elfeed-summary)
+  :straight t
+  :config
+  (setq elfeed-summary-filter-by-title t)
+  (setq elfeed-summary-skip-sync-tag 'skip))
+

elfeed-sync

+

elfeed-sync is my package to sync elfeed with tt-rss.

+
(use-package elfeed-sync
+  :straight (:host github :repo "SqrtMinusOne/elfeed-sync")
+  :if (not my/remote-server)
+  :after elfeed
+  :config
+  (elfeed-sync-mode)
+  (setq elfeed-sync-tt-rss-instance "https://sqrtminusone.xyz/tt-rss")
+  (setq elfeed-sync-tt-rss-login "sqrtminusone")
+  (setq elfeed-sync-tt-rss-password (my/password-store-get "Selfhosted/tt-rss")))
+

YouTube, podcasts & EMMS

+

Previously this block was opening MPV with start-process, but now I’ve managed to hook up MPV with EMMS. So there is the EMMS+elfeed “integration”.

+

There are multiple kinds of entries that I want to be opened by EMMS. First, a function that returns a YouTube URL:

+
(defun my/get-youtube-url (entry)
+  (let ((watch-id (cadr
+		   (assoc "watch?v"
+			  (url-parse-query-string
+			   (substring
+			    (url-filename
+			     (url-generic-parse-url (elfeed-entry-link entry)))
+			    1))))))
+    (when watch-id
+      (concat "https://www.youtube.com/watch?v=" watch-id))))
+

Second, a function that returns a URL to an enclosure. This is generally how podcasts are distributed.

+
(defun my/get-enclosures-url (entry)
+  (caar (elfeed-entry-enclosures entry)))
+

And a package called elfeed-tube to fetch some additional data from YouTUbe.

+
(use-package elfeed-tube
+  :straight t
+  :after elfeed
+  :config
+  (setq elfeed-tube-auto-fetch-p nil)
+  (elfeed-tube-setup)
+  (general-define-key
+   :states 'normal
+   :keymaps '(elfeed-search-mode-map elfeed-show-mode-map)
+   "gf" #'elfeed-tube-fetch))
+

Now, a function to add a YouTube link with metadata from elfeed to EMMS.

+
(with-eval-after-load 'emms
+  (define-emms-source elfeed (entry)
+    (let ((url (or (my/get-enclosures-url entry)
+		   (my/get-youtube-url entry))))
+      (unless url
+	(error "URL not found"))
+      (let ((track (emms-track 'url url)))
+	(emms-track-set track 'info-title (elfeed-entry-title entry))
+	(emms-playlist-insert-track track)))))
+
+(defun my/elfeed-add-emms ()
+  (interactive)
+  (emms-add-elfeed elfeed-show-entry)
+  (elfeed-tag elfeed-show-entry 'watched)
+  (elfeed-show-refresh))
+
+(with-eval-after-load 'elfeed
+  (general-define-key
+   :states '(normal)
+   :keymaps 'elfeed-show-mode-map
+   "gm" #'my/elfeed-add-emms))
+

rdrview

+

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:

+
(defun my/rdrview-get (url callback)
+  "Get the rdrview representation of URL.
+
+Call CALLBACK with the output."
+  (let* ((buffer (generate-new-buffer "rdrview"))
+	 (proc (start-process "rdrview" buffer "rdrview"
+			      url "-T" "title,sitename,body"
+			      "-H")))
+    (set-process-sentinel
+     proc
+     (lambda (process _msg)
+       (let ((status (process-status process))
+	     (code (process-exit-status process)))
+	 (cond ((and (eq status 'exit) (= code 0))
+		(progn
+		  (funcall callback
+			   (with-current-buffer (process-buffer process)
+			     (buffer-string)))
+		  (kill-buffer (process-buffer process))) )
+	       ((or (and (eq status 'exit) (> code 0))
+		    (eq status 'signal))
+		(let ((err (with-current-buffer (process-buffer process)
+			     (buffer-string))))
+		  (kill-buffer (process-buffer process))
+		  (user-error "Error in rdrview: %s" err)))))))
+    proc))
+

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:

+
(defun my/rdrview-parse (dom-string)
+  (let ((dom (with-temp-buffer
+	       (insert dom-string)
+	       (libxml-parse-html-region (point-min) (point-max)))))
+    (let (title sitename content (i 0))
+      (dolist (child (dom-children (car (dom-by-id dom "readability-page-1"))))
+	(when (listp child)
+	  (cond
+	   ((eq (car child) 'h1)
+	    (setq title (dom-text child)))
+	   ((eq (car child) 'h2)
+	    (setq sitename (dom-text child)))
+	   ((eq (car child) 'div)
+	    (setq content child)))))
+      (while (and
+	      (not (dom-by-tag content 'h1))
+	      (dom-search
+	       content
+	       (lambda (el)
+		 (when (listp el)
+		   (pcase (car el)
+		     ('h2 (setf (car el) 'h1))
+		     ('h3 (setf (car el) 'h2))
+		     ('h4 (setf (car el) 'h3))
+		     ('h5 (setf (car el) 'h4))
+		     ('h6 (setf (car el) 'h5))))))))
+      `((title . ,title)
+	(sitename . ,sitename)
+	(content . ,(with-temp-buffer
+		      (dom-print content)
+		      (buffer-string)))))))
+
Using rdrview from elfeed
+

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).
  • +
+ +
(defvar-local my/elfeed-show-rdrview-html nil)
+
+(defun my/rdrview-elfeed-show ()
+  (interactive)
+  (unless elfeed-show-entry
+    (user-error "No elfeed entry in this buffer!"))
+  (my/rdrview-get
+   (elfeed-entry-link elfeed-show-entry)
+   (lambda (result)
+     (let* ((data (my/rdrview-parse result))
+	    (inhibit-read-only t)
+	    (title (elfeed-entry-title elfeed-show-entry))
+	    (date (seconds-to-time (elfeed-entry-date elfeed-show-entry)))
+	    (authors (elfeed-meta elfeed-show-entry :authors))
+	    (link (elfeed-entry-link elfeed-show-entry))
+	    (tags (elfeed-entry-tags elfeed-show-entry))
+	    (tagsstr (mapconcat #'symbol-name tags ", "))
+	    (nicedate (format-time-string "%a, %e %b %Y %T %Z" date))
+	    (content (alist-get 'content data))
+	    (feed (elfeed-entry-feed elfeed-show-entry))
+	    (feed-title (elfeed-feed-title feed))
+	    (base (and feed (elfeed-compute-base (elfeed-feed-url feed)))))
+       (erase-buffer)
+       (insert (format (propertize "Title: %s\n" 'face 'message-header-name)
+		       (propertize title 'face 'message-header-subject)))
+       (when elfeed-show-entry-author
+	 (dolist (author authors)
+	   (let ((formatted (elfeed--show-format-author author)))
+	     (insert
+	      (format (propertize "Author: %s\n" 'face 'message-header-name)
+		      (propertize formatted 'face 'message-header-to))))))
+       (insert (format (propertize "Date: %s\n" 'face 'message-header-name)
+		       (propertize nicedate 'face 'message-header-other)))
+       (insert (format (propertize "Feed: %s\n" 'face 'message-header-name)
+		       (propertize feed-title 'face 'message-header-other)))
+       (when tags
+	 (insert (format (propertize "Tags: %s\n" 'face 'message-header-name)
+			 (propertize tagsstr 'face 'message-header-other))))
+       (insert (propertize "Link: " 'face 'message-header-name))
+       (elfeed-insert-link link link)
+       (insert "\n")
+       (cl-loop for enclosure in (elfeed-entry-enclosures elfeed-show-entry)
+		do (insert (propertize "Enclosure: " 'face 'message-header-name))
+		do (elfeed-insert-link (car enclosure))
+		do (insert "\n"))
+       (insert "\n")
+       (if content
+	   (elfeed-insert-html content base)
+	 (insert (propertize "(empty)\n" 'face 'italic)))
+       (setq-local my/elfeed-show-rdrview-html content)
+       (goto-char (point-min))))))
+

That way, calling M-x my/rdrview-elfeed-show replaces the original content with one from rdrview.

+
(with-eval-after-load 'elfeed
+  (general-define-key
+   :states '(normal)
+   :keymaps 'elfeed-show-mode-map
+   "gp" #'my/rdrview-elfeed-show))
+
How well does it work?
+

Rather ironically, it works well with sites that already ship with proper RSS, like Protesilaos Stavrou’s or Karthik Chikmagalur’s blogs or The Atlantic magazine.

+

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:

+
(setq my/rdrview-template (expand-file-name
+			   (concat user-emacs-directory "rdrview.tex")))
+

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-defun my/rdrview-render (content type variables callback
+				     &key file-name overwrite)
+  "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."
+  (unless file-name
+    (setq file-name (format "/tmp/%d.pdf" (random 100000000))))
+  (let (params
+	(temp-file-name (format "/tmp/%d.%s" (random 100000000) type)))
+    (cl-loop for (key . value) in variables
+	     when value
+	     do (progn
+		  (push "--variable" params)
+		  (push (format "%s=%s" key value) params)))
+    (setq params (nreverse params))
+    (if (and (file-exists-p file-name) (not overwrite))
+	(funcall callback file-name)
+      (with-temp-file temp-file-name
+	(insert content))
+      (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-status process))
+		 (code (process-exit-status process)))
+	     (cond ((and (eq status 'exit) (= code 0))
+		    (progn
+		      (message "Done!")
+		      (funcall callback file-name)))
+		   ((or (and (eq status 'exit) (> code 0))
+			(eq status '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:

+
(setq my/elfeed-pdf-dir (expand-file-name "~/.elfeed/pdf/"))
+
+(defun my/elfeed-open-pdf (entry overwrite)
+  "Open the current elfeed ENTRY with a pdf viewer.
+
+If OVERWRITE is non-nil, do the rendering even if the resulting
+PDF already exists."
+  (interactive (list elfeed-show-entry current-prefix-arg))
+  (let ((authors (mapcar (lambda (m) (plist-get m :name)) (elfeed-meta entry :authors)))
+	(feed-title (elfeed-feed-title (elfeed-entry-feed entry)))
+	(tags (mapconcat #'symbol-name (elfeed-entry-tags entry) ", "))
+	(date (format-time-string "%a, %e %b %Y"
+				  (seconds-to-time (elfeed-entry-date entry))))
+	(content (elfeed-deref (elfeed-entry-content entry)))
+	(file-name (concat my/elfeed-pdf-dir
+			   (elfeed-ref-id (elfeed-entry-content entry))
+			   ".pdf"))
+	(main-language "english")
+	(other-language "russian"))
+    (unless content
+      (user-error "No content!"))
+    (setq subtitle
+	  (cond
+	   ((seq-empty-p authors) feed-title)
+	   ((and (not (seq-empty-p (car authors)))
+		 (string-match-p (regexp-quote (car authors)) feed-title)) feed-title)
+	   (t (concat (string-join authors ", ") "\\\\" feed-title))))
+    (when (member 'ru (elfeed-entry-tags entry))
+      (setq main-language "russian")
+      (setq other-language "english"))
+    (my/rdrview-render
+     (if (bound-and-true-p my/elfeed-show-rdrview-html)
+	 my/elfeed-show-rdrview-html
+       content)
+     (elfeed-entry-content-type entry)
+     `((title . ,(elfeed-entry-title entry))
+       (subtitle . ,subtitle)
+       (date . ,date)
+       (tags . ,tags)
+       (main-lang . ,main-language)
+       (other-lang . ,other-language))
+     (lambda (file-name)
+       (start-process "xdg-open" nil "xdg-open" file-name))
+     :file-name file-name
+     :overwrite current-prefix-arg)))
+

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.

+
(with-eval-after-load 'elfeed
+  (general-define-key
+   :keymaps '(elfeed-show-mode-map)
+   :states '(normal)
+   "gv" #'my/elfeed-open-pdf))
+

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:

+
(defun my/get-languages (url)
+  (let ((main-lang "english")
+	(other-lang "russian"))
+    (when (string-match-p (rx ".ru") url)
+      (setq main-lang "russian"
+	    other-lang "english"))
+    (list main-lang other-lang)))
+
+(defun my/rdrview-open (url overwrite)
+  (interactive
+   (let ((url (read-from-minibuffer
+	       "URL: "
+	       (if (bound-and-true-p elfeed-show-entry)
+		   (elfeed-entry-link elfeed-show-entry)))))
+     (when (string-empty-p url)
+       (user-error "URL is empty"))
+     (list url current-prefix-arg)))
+  (my/rdrview-get
+   url
+   (lambda (res)
+     (let ((data (my/rdrview-parse res))
+	   (langs (my/get-languages url)))
+       (my/rdrview-render
+	(alist-get 'content data)
+	'html
+	`((title . ,(alist-get 'title data))
+	  (subtitle . ,(alist-get 'sitename data))
+	  (main-lang . ,(nth 0 langs))
+	  (other-lang . ,(nth 1 langs)))
+	(lambda (file-name)
+	  (start-process "xdg-open" nil "xdg-open" file-name)))))))
+

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-defun my/youtube-subtitles-get (video-id callback &key file-name overwrite)
+  "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-file file-name))
+		     :file-name nil
+		     :overwrite t))
+  (unless file-name
+    (setq file-name (format "/tmp/%d.vtt" (random 100000000))))
+  (if (and (file-exists-p file-name) (not overwrite))
+      (funcall callback file-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-status process))
+	       (code (process-exit-status process)))
+	   (cond ((and (eq status 'exit) (= code 0))
+		  (progn
+		    (with-current-buffer (process-buffer process)
+		      (setq buffer-file-name file-name)
+		      (save-buffer))
+		    (kill-buffer (process-buffer process))
+		    (funcall callback file-name)))
+		 ((or (and (eq status 'exit) (> code 0))
+		      (eq status 'signal))
+		  (let ((err (with-current-buffer (process-buffer process)
+			       (buffer-string))))
+		    (kill-buffer (process-buffer process))
+		    (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:

+
(setq my/elfeed-srt-dir (expand-file-name "~/.elfeed/srt/"))
+
+(defun my/elfeed-youtube-subtitles (entry &optional arg)
+  "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 (list elfeed-show-entry current-prefix-arg))
+  (let ((video-id (cadr
+		   (assoc "watch?v"
+			  (url-parse-query-string
+			   (substring
+			    (url-filename
+			     (url-generic-parse-url (elfeed-entry-link entry)))
+			    1))))))
+    (unless video-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-window file-name)
+	 (setq-local elfeed-show-entry entry)
+	 (goto-char (point-min))))
+     :file-name (concat my/elfeed-srt-dir
+			(elfeed-ref-id (elfeed-entry-content entry))
+			".vtt")
+     :overwrite arg)))
+

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:

+
(defun my/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 (list elfeed-show-entry))
+  (unless entry
+    (user-error "No entry!"))
+  (unless (derived-mode-p 'subed-mode)
+    (user-error "Not subed mode!"))
+  (setq-local subed-mpv-arguments
+	      (seq-uniq
+	       (append subed-mpv-arguments emms-player-mpv-parameters)))
+  (setq-local subed-mpv-video-file (elfeed-entry-link entry))
+  (subed-mpv--play subed-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.

+
(defun my/invoke-whisper--direct (input output-dir remove-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-directory output-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-status process))
+	     (code (process-exit-status process)))
+	 (cond ((and (eq status 'exit) (= code 0))
+		(notifications-notify :body "Audio conversion completed"
+				      :title "Whisper")
+		(when remove-wav
+		  (delete-file input))
+		(dolist (extension '(".txt" ".vtt" ".srt"))
+		  (rename-file (concat input extension)
+			       (concat (file-name-sans-extension input) extension)))
+		(kill-buffer (process-buffer process)))
+	       ((or (and (eq status 'exit) (> code 0))
+		    (eq status 'signal))
+		(let ((err (with-current-buffer (process-buffer process)
+			     (buffer-string))))
+		  (user-error "Error in Whisper: %s" err)))))))))
+
+(defun my/invoke-whisper (input output-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: " nil nil t)
+    (read-directory-name "Output directory: ")))
+  (if (string-match-p (rx ".wav" eos) input)
+      (my/invoke-whisper--direct input output-dir)
+    (let* ((ffmpeg-proc
+	    (start-process
+	     "ffmpef" nil "ffmpeg" "-i" input "-ar" "16000" "-ac" "1" "-c:a"
+	     "pcm_s16le" (concat (file-name-sans-extension input) ".wav"))))
+      (set-process-sentinel
+       ffmpeg-proc
+       (lambda (process _msg)
+	 (let ((status (process-status process))
+	       (code (process-exit-status process)))
+	   (cond ((and (eq status 'exit) (= code 0))
+		  (my/invoke-whisper--direct
+		   (concat (file-name-sans-extension input) ".wav") output-dir t))
+		 ((or (and (eq status 'exit) (> code 0))
+		      (eq status 'signal))
+		  (let ((err (with-current-buffer (process-buffer process)
+			       (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.

+
(with-eval-after-load 'elfeed
+  (defvar my/elfeed-whisper-podcast-files-directory
+    (concat elfeed-db-directory "/podcast-files/")))
+
+(defun my/elfeed-whisper-get-transcript-new (entry)
+  (interactive (list elfeed-show-entry))
+  (let* ((url (caar (elfeed-entry-enclosures entry)))
+	 (file-name (concat
+		     (elfeed-ref-id (elfeed-entry-content entry))
+		     "."
+		     (file-name-extension url)))
+	 (file-path (expand-file-name
+		     (concat
+		      my/elfeed-whisper-podcast-files-directory
+		      file-name))))
+    (message "Download started")
+    (unless (file-exists-p my/elfeed-whisper-podcast-files-directory)
+      (mkdir my/elfeed-whisper-podcast-files-directory))
+    (request url
+      :type "GET"
+      :encoding 'binary
+      :complete
+      (cl-function
+       (lambda (&key data &allow-other-keys)
+	 (let ((coding-system-for-write 'binary)
+	       (write-region-annotate-functions nil)
+	       (write-region-post-annotation-function nil))
+	   (write-region data nil file-path nil :silent))
+	 (message "Conversion started")
+	 (my/invoke-whisper file-path my/elfeed-srt-dir)))
+      :error
+      (cl-function
+       (lambda (&key error-thrown &allow-other-keys)
+	 (message "Error!: %S" error-thrown))))))
+

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:

+
(defun my/elfeed-show-related-files (entry)
+  (interactive (list elfeed-show-entry))
+  (let* ((files
+	  (mapcar
+	   (lambda (file) (cons (file-name-extension file) file))
+	   (seq-filter
+	    (lambda (file)
+	      (string-match-p
+	       (rx bos (literal (elfeed-ref-id (elfeed-entry-content entry))) ".")
+	       file))
+	    (directory-files my/elfeed-srt-dir))))
+	 (buffer
+	  (find-file-other-window
+	   (concat
+	    my/elfeed-srt-dir
+	    (alist-get
+	     (completing-read "File: " files)
+	     files nil nil #'equal)))))
+    (with-current-buffer buffer
+      (setq-local elfeed-show-entry entry))))
+

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.

+
(defun my/elfeed-whisper-get-transcript (entry)
+  "Retrieve transcript for the enclosure of the current elfeed ENTRY."
+  (interactive (list elfeed-show-entry))
+  (let ((enclosure (caar (elfeed-entry-enclosures entry))))
+    (unless enclosure
+      (user-error "No enclosure found!"))
+    (let ((srt-path (concat my/elfeed-srt-dir
+			    (elfeed-ref-id (elfeed-entry-content entry))
+			    ".srt")))
+      (if (file-exists-p srt-path)
+	  (let ((buffer (find-file-other-window srt-path)))
+	    (with-current-buffer buffer
+	      (setq-local elfeed-show-entry entry)))
+	(my/elfeed-whisper-get-transcript-new entry)))))
+
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:

+
(defun my/elfeed-whisper-subed (entry)
+  "Run MPV for the current Whisper-generated subtitles file.
+
+ENTRY is an instance of `elfeed-entry'."
+  (interactive (list elfeed-show-entry))
+  (unless entry
+    (user-error "No entry!"))
+  (unless (derived-mode-p 'subed-mode)
+    (user-error "Not subed mode!"))
+  (setq-local subed-mpv-video-file
+	      (expand-file-name
+	       (concat my/elfeed-whisper-podcast-files-directory
+		       (my/get-file-name-from-url
+			(caar (elfeed-entry-enclosures entry))))))
+  (subed-mpv--play subed-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.

+
(defun my/whisper-url (url file-name output-dir)
+  (interactive
+   (list (read-from-minibuffer "URL: ")
+	 (read-from-minibuffer "File name: ")
+	 (read-directory-name "Output directory: ")))
+  (let ((file-path
+	 (concat output-dir file-name "." (file-name-extension url))))
+    (message "Download started")
+    (request url
+      :type "GET"
+      :encoding 'binary
+      :complete
+      (cl-function
+       (lambda (&key data &allow-other-keys)
+	 (let ((coding-system-for-write 'binary)
+	       (write-region-annotate-functions nil)
+	       (write-region-post-annotation-function nil))
+	   (write-region data nil file-path nil :silent))
+	 (message "Conversion started")
+	 (my/invoke-whisper file-path output-dir)))
+      :error
+      (cl-function
+       (lambda (&key error-thrown &allow-other-keys)
+	 (message "Error!: %S" error-thrown))))))
+
Some observations
+

So, the functions above work for my purposes.

+

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.

+

Internet & Multimedia

+

Notmuch

+

My notmuch config now resides in Mail.org.

+
(unless (or my/is-termux my/remote-server)
+  (let ((mail-file (expand-file-name "mail.el" user-emacs-directory)))
+    (if (file-exists-p mail-file)
+	(load-file mail-file)
+      (message "Can't load mail.el"))))
+

Gnus

+

Gnus is an Emacs newsreader.

+

I’ll try to use it for NTTP for now. Will see if I can do more with it.

+
(use-package gnus
+  :straight t
+  :init
+  (my-leader-def "au" #'gnus)
+  :config
+  (my/persp-add-rule
+    gnus-summary-mode 0 "gnus"
+    ;; gnus-article-edit-mode 0 "gnus"
+    gnus-browse-mode 0 "gnus"
+    gnus-server-mode 0 "gnus"
+    gnus-article-mode 0 "gnus"
+    gnus-group-mode 0 "gnus"
+    gnus-category-mode 0 "gnus")
+  (let ((gnus-directory (concat user-emacs-directory "gnus")))
+    (unless (file-directory-p gnus-directory)
+      (make-directory gnus-directory))
+    (setq gnus-dribble-directory (concat gnus-directory "/dribble"))
+    (setq gnus-init-file (concat gnus-directory "/gnus.el"))
+    (setq gnus-startup-file (concat gnus-directory "/newsrc")))
+  ;; Sources
+  (setq gnus-select-method '(nntp "news.gwene.org"))
+  ;; Dribble
+  (setq gnus-always-read-dribble-file t)
+  ;; Agent
+  (setq gnus-agent-article-alist-save-format 1)
+  (setq gnus-agent-cache t))
+
Groups
+

Toggle current topic.

+
(defun my/gnus-topic-toggle-topic ()
+  (interactive "" gnus-topic-mode)
+  (when (gnus-group-topic-p)
+    (let ((topic (gnus-topic-find-topology (gnus-current-topic))))
+      (if (eq (cadadr topic) 'visible)
+	  (progn
+	    (gnus-topic-goto-topic (gnus-current-topic))
+	    (gnus-topic-remove-topic nil nil))
+	(gnus-topic-remove-topic t nil)))))
+

Custom keybindings.

+
(with-eval-after-load 'gnus-group
+  ;; Group
+  (add-hook 'gnus-group-mode-hook #'gnus-topic-mode)
+  (general-define-key
+   :states '(normal)
+   :keymaps '(gnus-group-mode-map)
+   "a" #'gnus-group-toggle-subscription-at-point)
+  (general-define-key
+   :states '(normal)
+   :keymaps '(gnus-topic-mode-map)
+   "TAB" #'my/gnus-topic-toggle-topic
+   "r" #'gnus-topic-catchup-articles))
+
Summary
+
(with-eval-after-load 'gnus-summary
+  (setq gnus-summary-line-format "%U%R%z%I%(%[%4L: %-23,23f%]%) %s\n")
+  (setq gnus-sum-thread-tree-false-root "> ")
+  (setq gnus-sum-thread-tree-indent "  ")
+  (setq gnus-sum-thread-tree-single-indent " ")
+  (setq gnus-sum-thread-tree-leaf-with-other "+-> ")
+  (setq gnus-sum-thread-tree-root "> ")
+  (setq gnus-sum-thread-tree-single-leaf "\\-> ")
+  (setq gnus-sum-thread-tree-vertical "| "))
+

EMMS

+

EMMS is the Emacs Multi-Media System. I use it to control MPD & MPV.

+

References:

+ + +
(use-package emms
+  :straight t
+  :if (not (or my/remote-server my/is-termux))
+  :commands (emms-smart-browse
+	     emms-browser
+	     emms-add-url
+	     emms-add-file
+	     emms-add-find)
+  :init
+  (my-leader-def
+    :infix "as"
+    "" '(:which-key "emms")
+    "s" (my/command-in-persp "EMMS" "EMMS" 0 (emms-smart-browse))
+    "b" #'emms-browser
+    "p" #'emms-pause
+    "q" #'emms-stop
+    ;; "h" #'emms-previous
+    ;; "l" #'emms-next
+    "u" #'emms-player-mpd-connect
+    "ww" #'emms-lyrics
+    "wb" #'emms-lyrics-toggle-display-on-minibuffer
+    "wm" #'emms-lyrics-toggle-display-on-modeline
+    "k" #'emms-volume-raise
+    "j" #'emms-volume-lower)
+  (my/persp-add-rule
+    emms-browser-mode 0 "EMMS"
+    emms-playlist-mode 0 "EMMS")
+  (setq emms-mode-line-icon-enabled-p nil)
+  :config
+  (require 'emms-setup)
+  (require 'emms-player-mpd)
+  (require 'emms-player-mpv)
+  (emms-all)
+  ;; MPD setup
+  <<emms-mpd-setup>>
+  ;; MPV setup
+  <<emms-mpv-setup>>
+  ;; evil-lion and evil-commentary shadow some gX bindings
+  ;; (add-hook 'emms-browser-mode-hook
+  ;; (lambda ()
+  ;; (evil-lion-mode -1)
+  ;; (evil-commentary-mode -1)
+  ;; ))
+  ;; <I've just read the line below as "I hate everything">
+  ;; I have everything I need in polybar
+  (emms-mode-line-mode -1)
+  (emms-playing-time-display-mode -1)
+  <<emms-fixes>>)
+
MPD
+

mpd is a server for playing music. It has a couple of first-class clients, including curses-based ncmpcpp, but of course, I want to use Emacs.

+
(setq emms-source-file-default-directory (expand-file-name "~/Music/"))
+(add-to-list 'emms-info-functions 'emms-info-mpd)
+(add-to-list 'emms-player-list 'emms-player-mpd)
+(setq emms-player-mpd-server-name "localhost")
+(setq emms-player-mpd-server-port "6600")
+(setq emms-player-mpd-music-directory "~/Music")
+

Connect on setup. For some reason, it stops the mpd playback whenever it connects, but it is not a big issue.

+
(emms-player-mpd-connect)
+

Clear MPD playlist on clearing EMMS playlist. IDK if this is fine for MPD library playlist, I don’t use them anyhow.

+
(add-hook 'emms-playlist-cleared-hook 'emms-player-mpd-clear)
+

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.

+
(emms-player-set emms-player-mpd
+		 'regex
+		 (rx (or (: "https://" (* nonl) (or "acast.com") (* nonl))
+			 (+ (? (or "https://" "http://"))
+			    (* nonl)
+			    (regexp (eval (emms-player-simple-regexp
+					   "m3u" "ogg" "flac" "mp3" "wav" "mod" "au" "aiff" "m4a")))))))
+

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-mpv t)
+

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.

+
(emms-player-set emms-player-mpv
+		 'regex
+		 (rx (or (: "https://" (* nonl) "youtube.com" (* nonl))
+			 (+ (? (or "https://" "http://"))
+			    (* nonl)
+			    (regexp (eval (emms-player-simple-regexp
+			    "mp4" "mov" "wmv" "webm" "flv" "avi" "mkv")))))))
+

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.

+
(setq my/youtube-dl-quality-list
+      '("bestvideo[height<=720]+bestaudio/best[height<=720]"
+	"bestvideo[height<=480]+bestaudio/best[height<=480]"
+	"bestvideo[height<=1080]+bestaudio/best[height<=1080]"))
+
+(setq my/default-emms-player-mpv-parameters
+      '("--quiet" "--really-quiet" "--no-audio-display"))
+
+(defun my/set-emms-mpd-youtube-quality (quality)
+  (interactive "P")
+  (unless quality
+    (setq quality (completing-read "Quality: " my/youtube-dl-quality-list nil t)))
+  (setq emms-player-mpv-parameters
+	`(,@my/default-emms-player-mpv-parameters ,(format "--ytdl-format=%s" quality))))
+
+(my/set-emms-mpd-youtube-quality (car my/youtube-dl-quality-list))
+

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.

+
(defun my/emms-cleanup-urls ()
+  (interactive)
+  (let ((keys-to-delete '()))
+    (maphash (lambda (key value)
+	       (when (eq (cdr (assoc 'type value)) 'url)
+		 (add-to-list 'keys-to-delete key)))
+	     emms-cache-db)
+    (dolist (key keys-to-delete)
+      (remhash key emms-cache-db)))
+  (setq emms-cache-dirty t))
+
+(my-leader-def "asc" #'my/emms-cleanup-urls)
+
Fetching lyrics
+

My package for fetching EMMS lyrics and album covers.

+
(use-package lyrics-fetcher
+  :straight t
+  :after (emms)
+  :init
+  (my-leader-def
+    "ast" #'lyrics-fetcher-show-lyrics
+    "asT" #'lyrics-fetcher-show-lyrics-query)
+  :config
+  (setq lyrics-fetcher-genius-access-token
+	(my/password-store-get "My_Online/APIs/genius.com"))
+  (general-define-key
+   :states '(emacs normal)
+   :keymaps 'emms-browser-mode-map
+   "gr" #'emms-browse-by-artist
+   "gl" 'lyrics-fetcher-emms-browser-show-at-point
+   "gC" 'lyrics-fetcher-emms-browser-fetch-covers-at-point
+   "go" 'lyrics-fetcher-emms-browser-open-large-cover-at-point)
+
+  (advice-add #'emms-lyrics-mode-line
+	      :override #'my/emms-lyrics-mode-line-override))
+

Also advice to change the location of the lyrics in the mode line.

+
(defun my/emms-lyrics-mode-line-override ()
+  (add-to-list 'global-mode-string
+	       '(:eval emms-lyrics-mode-line-string)))
+
+(defun my/emms-lyrics-restore-mode-line-override ()
+  "Restore the mode line."
+  (setq global-mode-string
+	    (remove '(:eval emms-lyrics-mode-line-string) global-mode-string))
+  (force-mode-line-update))
+
+(with-eval-after-load 'emms-lyrics
+  (advice-add #'emms-lyrics-mode-line
+	      :override #'my/emms-lyrics-mode-line-override)
+  (advice-add #'emms-lyrics-restore-mode-line
+	      :override #'my/emms-lyrics-restore-mode-line-override))
+
Some keybindings
+
(with-eval-after-load 'emms-browser
+  (general-define-key
+   :states '(normal)
+   :keymaps 'emms-browser-mode-map
+   "q" 'quit-window))
+
+(with-eval-after-load 'emms
+  (general-define-key
+   :states '(normal)
+   :keymaps 'emms-playlist-mode-map
+   "q" 'quit-window))
+
Setting volume
+
(defun my/set-volume (value)
+  (start-process "ponymix" nil "ponymix"
+		 (if (< 0 value) "increase" "decrease")
+		 (number-to-string (abs value))
+		 "--max-volume" "150"))
+
+(setq emms-volume-change-function #'my/set-volume)
+(setq emms-volume-change-amount 5)
+
EMMS & mpd Fixes
+

Some fixes until I submit a patch. I’ve submitted a patch for with these fixes, so I’ll remove this section eventually.

+

For some reason EMMS doesn’t fetch albumartist from MPD. Overriding this function fixes that.

+
(defun emms-info-mpd-process (track info)
+  (dolist (data info)
+    (let ((name (car data))
+	  (value (cdr data)))
+      (setq name (cond ((string= name "artist") 'info-artist)
+		       ((string= name "albumartist") 'info-albumartist)
+		       ((string= name "composer") 'info-composer)
+		       ((string= name "performer") 'info-performer)
+		       ((string= name "title") 'info-title)
+		       ((string= name "album") 'info-album)
+		       ((string= name "track") 'info-tracknumber)
+		       ((string= name "disc") 'info-discnumber)
+		       ((string= name "date") 'info-year)
+		       ((string= name "genre") 'info-genre)
+		       ((string= name "time")
+			(setq value (string-to-number value))
+			'info-playing-time)
+		       (t nil)))
+      (when name
+	(emms-track-set track name value)))))
+

Also, emms-player-mpd-get-alists has an interesting bug. This function parses the response to listallinfo, which looks something like this:

+
tag1: value1
+tag2: value2
+...
+tag1: value1'
+tag2: value2'
+

This structure has to be converted to list of alists, which looks like:

+
(("tag1" . "value1"
+  "tag2" . "value2")
+  ("tag1" . "value1'"
+  ("tag2" . "value2'")))
+

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.

+
(defun emms-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 (and info
+	     (null (car info))          ; no error has occurred
+	     (cdr info))                ; data exists
+    (let ((alists nil)
+	  (alist nil)
+	  cell)
+      (dolist (line (cdr info))
+	(when (setq cell (emms-player-mpd-parse-line line))
+	  (if (member (car cell) '("file" "directory" "playlist"))
+	      (setq alists (cons alist alists)
+		    alist (list cell))
+	    (setq alist (cons cell alist)))))
+      (when alist
+	(setq alists (cons alist alists)))
+      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.

+
(use-package ytel
+  :straight t
+  :commands (ytel)
+  :config
+  (setq ytel-invidious-api-url "https://invidio.xamh.de/")
+  (general-define-key
+   :states '(normal)
+   :keymaps 'ytel-mode-map
+   "q" #'ytel-quit
+   "s" #'ytel-search
+   "L" #'ytel-search-next-page
+   "H" #'ytel-search-previous-page
+   "RET" #'my/ytel-add-emms))
+
EMMS integration
+

And here is the same kind of integration with EMMS as in the elfeed setup:

+
(with-eval-after-load 'emms
+  (define-emms-source ytel (video)
+    (let ((track (emms-track
+		  'url (concat "https://www.youtube.com/watch?v="
+			       (ytel-video-id video)))))
+      (emms-track-set track 'info-title (ytel-video-title video))
+      (emms-track-set track 'info-artist (ytel-video-author video))
+      (emms-playlist-insert-track track))))
+
+(defun my/ytel-add-emms ()
+  (interactive)
+  (emms-add-ytel (ytel-get-current-video)))
+
Choosing instances
+

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.

+

Inspired by this comment.

+
(setq my/invidious-instances-url
+      "https://api.invidious.io/instances.json?pretty=1&sort_by=health")
+
(defun my/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-synchronously my/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)))))
+
+(defun my/ytel-instances-alist-from-json ()
+  "Make the json of invidious instances into an alist."
+  (let ((jsonlist (my/ytel-instances-fetch-json))
+	(inst ()))
+    (while jsonlist
+      (push (concat "https://" (caar jsonlist)) inst)
+      (setq jsonlist (cdr jsonlist)))
+    (nreverse inst)))
+
+(defun my/ytel-choose-instance ()
+  "Prompt user to choose an invidious instance to use."
+  (interactive)
+  (setq ytel-invidious-api-url
+	(or (condition-case nil
+		(completing-read "Using instance: "
+				 (cl-subseq (my/ytel-instances-alist-from-json) 0 11) nil "confirm" "https://")
+	      (error nil))
+	    "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.

+

So, let’s skip videos with null titles.

+
(defun my/ytel-draw--buffer-nil-videos-fix ()
+  (let ((inhibit-read-only t)
+	    (current-line      (line-number-at-pos)))
+    (erase-buffer)
+    (setf header-line-format
+	  (concat "Search results for "
+				  (propertize ytel-search-term 'face 'ytel-video-published-face)
+				  ", page "
+				  (number-to-string ytel-current-page)))
+    (seq-do
+     (lambda (v)
+	   (ytel--insert-video v)
+	   (insert "\n"))
+     (seq-filter
+      (lambda (v)
+	(ytel-video-title v))
+      ytel-videos))
+    (goto-char (point-min))))
+
+(with-eval-after-load 'ytel
+  (advice-add #'ytel--draw-buffer :override #'my/ytel-draw--buffer-nil-videos-fix))
+

And render other potentially null fields as “unknown”.

+
(defun my/ytel--format-unknown-fix (fun &rest args)
+  (if (car args)
+      (apply fun args)
+    "unknown   "))
+
+(with-eval-after-load 'ytel
+  (advice-add #'ytel--format-video-length :around #'my/ytel--format-unknown-fix)
+  (advice-add #'ytel--format-video-published :around #'my/ytel--format-unknown-fix)
+  (advice-add #'ytel--format-video-views :around #'my/ytel--format-unknown-fix))
+
Some functions
+

Also, a function to copy a URL to the video under cursor.

+
(defun my/ytel-kill-url ()
+  (interactive)
+  (kill-new
+   (concat
+    "https://www.youtube.com/watch?v="
+    (ytel-video-id (ytel-get-current-video)))))
+

EWW

+

Emacs built-in web browser. I wonder if anyone actually uses it.

+

I use it occasionally to open links in elfeed.

+

Toggle using fonts in buffer:

+
(defun my/toggle-shr-use-fonts ()
+  "Toggle the shr-use-fonts variable in buffer"
+  (interactive)
+  (setq-local shr-use-fonts (not shr-use-fonts)))
+

Setting the default font.

+
(defface my/shr-face
+  `((t :inherit variable-pitch))
+  "Default face for shr rendering.")
+
+(my/use-colors
+  (my/shr-face :foreground (my/color-value 'blue)))
+
+(defun my/shr-insert-around (fun &rest args)
+  (let ((shr-current-font (or shr-current-font 'my/shr-face)))
+    (apply fun args)))
+
+(defun my/shr-urlify-around (fun start url &optional title)
+  (funcall fun start url title)
+  (let ((faces (get-text-property start 'face)))
+    (put-text-property
+     start (point)
+     'face
+     (mapcar
+      (lambda (face)
+	(if (eq face 'my/shr-face)
+	    'link
+	  face))
+      (if (sequencep faces) faces (list faces))))))
+
+(with-eval-after-load 'shr
+  (advice-add #'shr-insert :around #'my/shr-insert-around)
+  (advice-add #'shr-urlify :around #'my/shr-urlify-around))
+
(my-leader-def "aw" 'eww)
+(my/persp-add-rule
+  eww-mode 2 "browser")
+
+(with-eval-after-load 'eww
+  (general-define-key
+   :keymaps '(eww-mode-map)
+   :states '(normal emacs)
+   "f" #'ace-link-eww
+   "+" 'text-scale-increase
+   "-" 'text-scale-decrease))
+

ERC

+

ERC is a built-it Emacs IRC client.

+
(use-package erc
+  :commands (erc erc-tls)
+  :straight (:type built-in)
+  :config
+  (setq erc-log-channels-directory "~/.erc/logs")
+  (setq erc-save-buffer-on-part t)
+  (add-to-list 'erc-modules 'autojoin)
+  (add-to-list 'erc-modules 'notifications)
+  (add-to-list 'erc-modules 'log)
+  (erc-update-modules)
+  (setq erc-autojoin-channels-alist
+	`((,(rx "libera.chat")
+	   "#systemcrafters" "#systemcrafters-emacs")))
+  (setq erc-kill-buffer-on-part t)
+  (setq erc-track-shorten-start 8))
+

Exclude everything but actual messages from notifications.

+
(setq erc-track-exclude-types '("NICK" "JOIN" "LEAVE" "QUIT" "PART"
+				"301"   ; away notice
+				"305"   ; return from awayness
+				"306"   ; set awayness
+				"324"   ; modes
+				"329"   ; channel creation date
+				"332"   ; topic notice
+				"333"   ; who set the topic
+				"353"   ; Names notice
+				))
+

A plugin to highlight IRC nicknames:

+
(use-package erc-hl-nicks
+  :hook (erc-mode . erc-hl-nicks-mode)
+  :after (erc)
+  :straight t)
+

ZNC support. Seems to provide a few nice features for ZNC.

+
(use-package znc
+  :straight t
+  :commands (znc-erc)
+  :init
+  ;; (my-leader-def "ai" #'znc-erc)
+  (my/persp-add-rule
+    erc-mode 3 "ERC")
+  :config
+  (setq znc-servers
+	`(("sqrtminusone.xyz" 6697 t
+	   ((libera "sqrtminusone"
+		    ,(my/password-store-get "Selfhosted/ZNC")))))))
+

Send /detach to all servers. Kinda strange that there’s no such function already

+
(defun my/erc-detach-all ()
+  (interactive)
+  (cl-loop for buf being the buffers
+	   if (eq (buffer-local-value 'major-mode buf) 'erc-mode)
+	   do (with-current-buffer buf
+		(when (erc-server-process-alive)
+		  (let ((tgt (erc-default-target)))
+		    (erc-server-send (format "DETACH %s" tgt) nil tgt))))))
+

Mastodon

+

Mastodon is a decentralized social media network. I use an instance called emacs.ch.

+
Package configuration
+

mastodon.el is an Emacs client for Mastodon.

+

The default UI is rather rough, but Nicolas Rougier’s mastodon-alt package makes things a bit more how I would like to see them.

+
(use-package mastodon
+  :straight t
+  :commands (my/mastodon)
+  :init
+  (my-leader-def "an" #'my/mastodon)
+  :config
+  (setq mastodon-instance-url "https://emacs.ch")
+  (setq mastodon-active-user "sqrtminusone")
+  (my/persp-add-rule mastodon-mode 0 "mastodon")
+  ;; Hide spoilers by default
+  (setq-default mastodon-toot--content-warning t)
+  (setq mastodon-media--avatar-height 40)
+  (setq mastodon-tl--show-avatars t)
+  ;; The default emojis take two characters for me
+  (setq mastodon-tl--symbols
+	'((reply "" . "R")
+	  (boost "" . "B")
+	  (favourite "" . "F")
+	  (bookmark "" . "K")
+	  (media "" . "[media]")
+	  (verified "" . "V")
+	  (locked "" . "[locked]")
+	  (private "" . "[followers]")
+	  (direct "" . "[direct]")
+	  (edited "" . "[edited]"))))
+
+(use-package mastodon-alt
+  :straight (:host github :repo "rougier/mastodon-alt")
+  :after (mastodon)
+  :config
+  (mastodon-alt-tl-activate))
+
+(use-package transient
+  :straight t
+  :defer t)
+
UI and keymaps
+

display-line-numbers-mode screws the UI for some reason.

+
(defun my/mastodon-configure ()
+  (display-line-numbers-mode -1))
+
+(add-hook 'mastodon-mode-hook #'my/mastodon-configure)
+

Kill processes. Useful when the package stops working due to unstable connection.

+
(defun my/mastodon-reset ()
+  (interactive)
+  (cl-loop for process in (process-list)
+	   if (string-match-p "emacs.ch" (process-name process))
+	   do (delete-process process)))
+

The package also doesn’t have evil bindings. I implement a few basic bindings here:

+
(with-eval-after-load 'mastodon
+  (general-define-key
+   :states '(normal motion)
+   :keymaps '(mastodon-mode-map)
+   "J" #'mastodon-tl--goto-next-toot
+   "K" #'mastodon-tl--goto-prev-toot
+   "M-j" #'mastodon-tl--next-tab-item
+   "M-k" #'mastodon-tl--prev-tab-item
+   "<tab>" #'mastodon-tl--next-tab-item
+   "<backtab>" #'mastodon-tl--previous-tab-item
+   "o" #'my/mastodon-toot
+   "r" 'mastodon-tl--update
+   "c" #'mastodon-tl--toggle-spoiler-text-in-toot
+   "q" #'kill-current-buffer))
+
Modeline segment
+

This is my attempt to make a modeline indicator for new mastodon notifications.

+

Edit [2023-07-28 Fri]: I’ll probably remove that, don’t feel like it’s actually useful.

+
(defvar my/mastodon-mode-string "")
+
+(defvar my/mastodon-mode-line-unread-ids nil)
+
+(defvar my/mastodon-mode-line-saved-ids nil)
+
+(defvar my/mastodon-mode-line-timer nil)
+
+(defvar my/mastodon-mode-line-file
+  (concat no-littering-var-directory "mastodon/notif-ids"))
+
+(defun my/mastodon-mode-line-load-meta ()
+  (when (file-exists-p my/mastodon-mode-line-file)
+    (ignore-errors
+      (with-temp-buffer
+	(insert-file-contents my/mastodon-mode-line-file)
+	(setq my/mastodon-mode-line-saved-ids
+	      (read (current-buffer)))))))
+
+(defun my/mastodon-mode-line-persist-meta ()
+  (mkdir (file-name-directory my/mastodon-mode-line-file) t)
+  (let ((coding-system-for-write 'utf-8))
+    (ignore-errors
+      (with-temp-file my/mastodon-mode-line-file
+	(let ((standard-output (current-buffer))
+	      (print-level nil)
+	      (print-length nil)
+	      (print-circle nil))
+	  (princ ";;; Mastodon Saved Notifications\n\n")
+	  (prin1 my/mastodon-mode-line-saved-ids))))))
+
+(defun my/mastodon-mode-line-update ()
+  (if my/mastodon-mode-line-unread-ids
+      (setq my/mastodon-mode-string
+	    (concat "["
+		    (propertize (number-to-string
+				 (length my/mastodon-mode-line-unread-ids))
+				'face 'success)
+		    "]"))
+    (setq my/mastodon-mode-string "")))
+
+(defun my/mastodon-mode-line-update-fetch ()
+  (mastodon-http--get-json-async
+   (mastodon-http--api "notifications") nil
+   (lambda (data)
+     (let ((fetched-ids
+	    (cl-loop for datum in data collect (alist-get 'id datum))))
+       (setq my/mastodon-mode-line-unread-ids
+	     (seq-difference fetched-ids my/mastodon-mode-line-saved-ids))
+       (setq my/mastodon-mode-line-saved-ids
+	     (seq-intersection my/mastodon-mode-line-saved-ids fetched-ids)))
+     (my/mastodon-mode-line-update))))
+
+(defun my/mastodon-notifications--timeline-before (toots)
+  (let* ((all-ids (seq-uniq
+		   (append
+		    my/mastodon-mode-line-saved-ids
+		    (cl-loop for datum in toots
+			     collect (alist-get 'id datum))))))
+    (setq my/mastodon-mode-line-unread-ids
+	  (seq-difference my/mastodon-mode-line-unread-ids all-ids))
+    (setq my/mastodon-mode-line-saved-ids all-ids))
+  (my/mastodon-mode-line-update))
+
+(with-eval-after-load 'mastodon
+  (define-minor-mode my/mastodon-mode-line
+    "Display mastodon notification count in mode line."
+    :require 'mastodon
+    :global t
+    :group 'mastodon
+    :after-hook
+    (progn
+      (when (timerp my/mastodon-mode-line-timer)
+	(cancel-timer my/mastodon-mode-line-timer))
+      (if my/mastodon-mode-line
+	  (progn
+	    (add-to-list 'mode-line-misc-info '(:eval my/mastodon-mode-string) t)
+	    (my/mastodon-mode-line-load-meta)
+	    (setq my/mastodon-mode-line-timer
+		  (run-with-timer 0 150 #'my/mastodon-mode-line-update-fetch))
+	    (advice-add #'mastodon-notifications--timeline :before
+			#'my/mastodon-notifications--timeline-before)
+	    (add-hook 'kill-emacs-hook #'my/mastodon-mode-line-persist-meta))
+	(setq mode-line-misc-info (delete '(:eval my/mastodon-mode-string)
+					  mode-line-misc-info))
+	(advice-remove #'mastodon-notifications--timeline
+		       #'my/mastodon-notifications--timeline-before)
+	(remove-hook 'kill-emacs-hook #'my/mastodon-mode-line-persist-meta)
+	(my/mastodon-mode-line-persist-meta)))))
+
Timeline Transient
+

The default mastodon-tl--get-home-timeline allows only to hide replies, and not boosted posts.

+

So here’s a custom update function:

+
(defun my/mastodon-get-update-funciton (hide-replies hide-boosts)
+  (lambda (toots)
+    (let* ((is-profile (eq (mastodon-tl--get-buffer-type) 'profile-statuses))
+	   (hide-replies (and (not is-profile) hide-replies))
+	   (hide-boosts (and (not is-profile) hide-boosts))
+	   (toots (seq-filter
+		   (lambda (toot)
+		     (and
+		      (or (not hide-replies)
+			  ;; Why is the original function inverted??
+			  (mastodon-tl--is-reply toot))
+		      (or (not hide-boosts)
+			  (not (alist-get 'reblog toot)))))
+		   toots)))
+      (mapc #'mastodon-tl--toot toots))))
+

In order to use it, the function has to be passed to mastodon-tl--init:

+
(defun my/mastodon-tl--get-home (hide-replies hide-boosts)
+  (mastodon-tl--init
+   "home"
+   "timelines/home"
+   (my/mastodon-get-update-funciton hide-replies hide-boosts)
+   nil
+   `(("limit" . ,mastodon-tl--timeline-posts-count))
+   nil))
+

And a transient to use it.

+
(with-eval-after-load 'mastodon
+  (require 'transient)
+  (transient-define-prefix my/mastodon-tl ()
+    ["Home timeline params"
+     ("-r" "--hide-replies" "--hide-replies" :init-value
+      (lambda (obj) (oset obj value "--hide-replies")))
+     ("-b" "--hide-boosts" "--hide-boosts" :init-value
+      (lambda (obj) (oset obj value "--hide-boosts")))]
+    ["Timelines"
+     :class transient-row
+     ("t" "Home" (lambda (args)
+		   (interactive (list (transient-args transient-current-command)))
+		   (my/mastodon-tl--get-home
+		    (seq-contains-p args "--hide-replies")
+		    (seq-contains-p args "--hide-boosts"))))
+     ("l" "Local" mastodon-tl--get-local-timeline)
+     ("f" "Federated" mastodon-tl--get-federated-timeline)
+     ("g" "One tag" mastodon-tl--get-tag-timeline)
+     ("a" "Followed tags" mastodon-tl--followed-tags-timeline)
+     ("s" "Some followed tags" mastodon-tl--some-followed-tags-timeline)]
+    ["Misc"
+     :class transient-row
+     ("q" "Quit" transient-quit-one)]))
+
Main Transient
+

Also, there are so many commands that it’s hard to remember all of them. So I define two transient prefixes.

+

The first dispatches “general” actions:

+
(with-eval-after-load 'mastodon
+  (require 'transient)
+  (transient-define-prefix my/mastodon ()
+    "Mastodon."
+    ["Various views"
+     :class transient-row
+     ("m" "Mastodon" mastodon)
+     ("t" "Timelines" my/mastodon-tl)
+     ("n" "Notifications" mastodon-notifications-get)
+     ("s" "Search query" mastodon-search--search-query)]
+    ["Tags"
+     :class transient-row
+     ("aa" "Followed tags" mastodon-tl--list-followed-tags)
+     ("af" "Follow tag" mastodon-tl--follow-tag)
+     ("aF" "Unfollow tag" mastodon-tl--unfollow-tag)]
+    ["Own profile"
+     :class transient-row
+     ("c" "Toot" mastodon-toot)
+     ("o" "My profile" mastodon-profile--my-profile)
+     ("u" "Update profile note" mastodon-profile--update-user-profile-note)
+     ("f" "Favourites" mastodon-profile--view-favourites)
+     ("b" "Bookmarks" mastodon-profile--view-bookmarks)]
+    ["Minor views"
+     :class transient-row
+     ("F" "Follow requests" mastodon-views--view-follow-requests)
+     ("S" "Scheduled toots" mastodon-views--view-scheduled-toots)
+     ("I" "Filters" mastodon-views--view-filters)
+     ("G" "Follow suggestions" mastodon-views--view-follow-suggestions)
+     ("L" "Lists" mastodon-views--view-lists)]
+    ["Misc"
+     :class transient-row
+     ("/" "Switch to buffer" mastodon-switch-to-buffer)
+     ("Q" "Kill all buffers" mastodon-kill-all-buffers)
+     ("q" "Quit" transient-quit-one)]))
+
Toot Transient
+

And the second one dispatches actions related to particular toot / profile.

+

Also, some actions don’t have any confirmations, so here’s a macro that wraps a function with y-or-n-p:

+
(defmacro my/def-confirmer (func text)
+  `(defun ,(intern (concat "my/" (symbol-name func) "-confirm")) ()
+     (interactive)
+     (when (y-or-n-p ,text)
+       (call-interactively #',func))))
+

A function to open the toot in browser:

+
(defun my/mastodon-toot--browse ()
+  "Copy URL of toot at point.
+If the toot is a fave/boost notification, copy the URLof the
+base toot."
+  (interactive)
+  (let* ((toot (or (mastodon-tl--property 'base-toot)
+		   (mastodon-tl--property 'toot-json)))
+	 (url (if (mastodon-tl--field 'reblog toot)
+		  (alist-get 'url (alist-get 'reblog toot))
+		(alist-get 'url toot))))
+    (browse-url url)))
+

And the prefix itself:

+
(with-eval-after-load 'mastodon
+  (my/def-confirmer mastodon-toot--toggle-boost "Toggle boost for this post? ")
+  (my/def-confirmer mastodon-toot--toggle-favourite "Toggle favourite this post? ")
+  (my/def-confirmer mastodon-toot--toggle-bookmark "Toggle bookmark this post? ")
+  (my/def-confirmer mastodon-tl--follow-user "Follow this user? ")
+  (my/def-confirmer mastodon-tl--unfollow-user "Unfollow this user? ")
+  (my/def-confirmer mastodon-tl--block-user "Block this user? ")
+  (my/def-confirmer mastodon-tl--unblock-user "Unblock this user? ")
+  (my/def-confirmer mastodon-tl--mute-user "Mute this user? ")
+  (my/def-confirmer mastodon-tl--unmute-user "Unmute this user? ")
+  (my/def-confirmer mastodon-tl--unmute-user "Unmute this user? ")
+
+  (transient-define-prefix my/mastodon-toot ()
+    "Mastodon toot actions."
+    ["View"
+     :class transient-row
+     ("o" "Thread" mastodon-tl--thread)
+     ("w" "Browser" my/mastodon-toot--browse)
+     ("le" "List edits" mastodon-toot--view-toot-edits)
+     ("lf" "List favouriters" mastodon-toot--list-toot-favouriters)
+     ("lb" "List boosters" mastodon-toot--list-toot-boosters)]
+    ["Toot Actions"
+     :class transient-row
+     ("r" "Reply" mastodon-toot--reply)
+     ("v" "Vote" mastodon-tl--poll-vote)
+     ("b" "Boost" my/mastodon-toot--toggle-boost-confirm)
+     ("f" "Favourite" my/mastodon-toot--toggle-favourite-confirm)
+     ("k" "Bookmark" my/mastodon-toot--toggle-bookmark-confirm)]
+    ["My Toot Actions"
+     :class transient-row
+     ("md" "Delete" mastodon-toot--delete-toot)
+     ("mD" "Delete and redraft" mastodon-toot--delete-and-redraft-toot)
+     ("mp" "Pin" mastodon-toot--pin-toot-toggle)
+     ("me" "Edit" mastodon-toot--edit-toot-at-point)]
+    ["Profile Actions"
+     :class transient-row
+     ("pp" "Profile" mastodon-profile--show-user)
+     ("pf" "List followers" mastodon-profile--open-followers)
+     ("pF" "List following" mastodon-profile--open-following)
+     ("ps" "List statues (no reblogs)" mastodon-profile--open-statuses-no-reblogs)]
+    ["User Actions"
+     :class transient-row
+     ("uf" "Follow user" my/mastodon-tl--follow-user-confirm)
+     ("uF" "Unfollow user" my/mastodon-tl--unfollow-user-confirm)
+     ("ub" "Block user" my/mastodon-tl--block-user-confirm)
+     ("uB" "Unblock user" my/mastodon-tl--unblock-user-confirm)
+     ("um" "Mute user" my/mastodon-tl--mute-user-confirm)
+     ("uB" "Unmute user" my/mastodon-tl--unmute-user-confirm)]
+    ["Misc"
+     :class transient-row
+     ("q" "Quit" transient-quit-one)]))
+

ement.el

+

ement.el is a Matrix client for Emacs. This package turned out to be somewhat complicated to setup.

+
General config
+
(use-package plz
+  :straight (:host github :repo "alphapapa/plz.el")
+  :defer t)
+
+(defun my/ement ()
+  (interactive)
+  (ement-connect
+   :user-id "@sqrtminusone:matrix.org"
+   :password (my/password-store-get "My_Online/Accounts/matrix")))
+
+(use-package ement
+  :straight (:host github :repo "alphapapa/ement.el")
+  :commands (ement-connect)
+  :init
+  (my-leader-def "ai" #'my/ement)
+  :config
+  (setq ement-room-list-auto-update t)
+  (setq ement-room-mark-rooms-read 'send)
+  (my/persp-add-rule
+    ement-room-mode 3 "ement"
+    ement-describe-room-mode 3 "ement"
+    ement-room-occur-mode 3 "ement"
+    ement-room-list-mode 3 "ement")
+  ;; Room UI
+  (setq ement-room-message-format-spec "%S> %W%B%r%R[%t]")
+  (setq ement-room-left-margin-width 0)
+  (setq ement-room-right-margin-width 10)
+  (setq ement-room-sender-in-left-margin nil)
+  (setq ement-room-sender-headers nil)
+  (setq ement-room-sender-in-headers nil)
+  (setq ement-room-wrap-prefix "-> ")
+  ;; Changing some default faces
+  (set-face-attribute 'ement-room-reactions nil :height 'unspecified)
+  (set-face-attribute 'ement-room-reactions-key nil :height 'unspecified)
+  (set-face-attribute 'ement-room-timestamp nil :inherit 'font-lock-function-name-face)
+  (set-face-attribute 'ement-room-membership nil :height 0.9
+		      :inherit 'font-lock-warning-face)
+  (set-face-attribute 'ement-room-wrap-prefix nil :inherit 'unspecified)
+  (set-face-attribute 'ement-room-timestamp-header nil :height 'unspecified)
+  (set-face-attribute 'ement-room-wrap-prefix nil :inherit 'unspecified)
+  ;; Notify only on mentions
+  (setq ement-notify-notification-predicates
+	'(ement-notify--event-mentions-session-user-p
+	  ement-notify--event-mentions-room-p
+	  ement-notify--room-unread-p))
+  ;; Fix the anti-synergy with major mode re-activation in `ement-room-list-revert'
+  (advice-add #'ement-room-list-revert
+	      :around #'my/perspective-assign-ignore-advice))
+
Keybindings
+

Some custom keymaps for room lists:

+
(with-eval-after-load 'ement-room-list
+  (general-define-key
+   :states '(normal visual)
+   :keymaps '(ement-room-list-mode-map)
+   "<tab>" #'magit-section-toggle
+   "C-j" #'magit-section-forward
+   "C-k" #'magit-section-backward
+   "q" #'quit-window
+   "gr" #'revert-buffer
+   "RET" #'ement-room-list-RET))
+
+(with-eval-after-load 'ement-tabulated-room-list
+  (general-define-key
+   :states '(normal visual)
+   :keymaps '(ement-tabulated-room-list-mode-map)
+   "q" #'quit-window))
+
(defun my/ement-room-send-reaction (key position)
+  (interactive (list
+		(completing-read "Add reaction: " (append telega-emoji-reaction-list '("👋")))
+		(point)))
+  (ement-room-send-reaction key position))
+
+(defun my/ement-room-compose-quit ()
+  (interactive)
+  (when (or (string-empty-p (buffer-string))
+	    (y-or-n-p "Quit compose? "))
+    (quit-window t)))
+
+(defun my/ement-room-compose-setup ()
+  (ement-room-compose-org)
+  (setq company-backends '(telega-company-emoji company-capf))
+  (general-define-key
+   :states '(normal visual)
+   :keymaps 'local
+   "Q" #'my/ement-room-compose-quit
+   "C-c C-k" (lambda () (interactive) (quit-window t))
+   "C-c C-c" #'ement-room-compose-send))
+
+(add-hook 'ement-room-compose-hook #'my/ement-room-compose-setup)
+

Also a keymap for room mode:

+
(with-eval-after-load 'ement
+  (general-define-key
+   :states '(normal visual)
+   :keymaps '(ement-room-mode-map)
+   "q" #'quit-window
+   "?" #'ement-room-transient
+   "C-u" #'ement-room-scroll-down-command
+   "C-d" #'ement-room-scroll-up-mark-read
+   "r" #'ement-room-write-reply
+   "a" #'ement-room-send-message
+   "i" #'ement-room-send-message
+   "e" #'ement-room-edit-message
+   "M-<RET>" #'ement-room-compose-message
+   "<RET>" #'ement-room-send-message
+   "K" #'ement-room-goto-prev
+   "J" #'ement-room-goto-next
+   "gr" #'ement-room-sync
+   "g?" #'ement-describe-room
+   "R?" #'ement-describe-room
+   "Rm" #'ement-list-members
+   "Rn" #'ement-room-set-notification-state
+   "Rt" #'ement-room-set-topic
+   "!" #'my/ement-room-send-reaction
+   "m?" #'ement-room-view-event
+   "Zf" #'ement-room-send-file
+   "ui" #'ement-invite-user)
+  (general-define-key
+   :states '(normal visual)
+   :keymaps '(ement-describe-room-mode-map)
+   "q" #'quit-window)
+  (general-define-key
+   :states '(motion)
+   :keymaps '(ement-room-mode-map)
+   "C-u" #'ement-room-scroll-down-command
+   "C-d" #'ement-room-scroll-up-mark-read))
+
Various functions
+

Scroll to the previous mention.

+
+

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.

+
+
(defun my/ement-about-me-p (event)
+  (let ((me (ement-user-id (ement-session-user ement-session))))
+    (or
+     (equal (ement-user-id (ement-event-sender event)) me)
+     (when-let ((formatted-body
+		 (alist-get
+		  'formatted_body
+		  (ement-event-content event))))
+       (string-match-p me formatted-body)))))
+
+(defun my/ement-scroll-to-previous-about-me ()
+  (interactive)
+  (let ((scrolled 0))
+    (when (< (line-number-at-pos) 20)
+      (forward-line 20))
+    (if ement-room-retro-loading
+	(run-with-timer 0.5 nil #'my/ement-scroll-to-previous-about-me)
+      (while (let ((event (ewoc-data (ewoc-locate ement-ewoc))))
+	       (and
+		(not ement-room-retro-loading)
+		(or
+		 (not (ement-event-p event))
+		 (not (my/ement-about-me-p event)))))
+	(condition-case _err
+	    (scroll-down 1)
+	  (beginning-of-buffer
+	   (call-interactively #'ement-room-retro)
+	   (run-with-timer 0.5 nil #'my/ement-scroll-to-previous-about-me)))
+	(cl-incf scrolled)
+	(message "Scrolled %s" scrolled)))))
+

Telega

+

telega.el is a Telegam client for Emacs.

+ + + + + + + + + + + + + + + + + +
Guix dependency
tdlib-1.8.16
font-gnu-unifont
font-gnu-freefont
+
(use-package telega
+  :straight t
+  :if (not (or my/remote-server my/is-termux))
+  :commands (telega)
+  :init
+  (my-leader-def "a l" (my/command-in-persp "telega" "telega" 3 (telega)))
+  (my/use-colors
+   (telega-button-active :foreground (my/color-value 'base0)
+			 :background (my/color-value 'cyan))
+   (telega-webpage-chat-link :foreground (my/color-value 'base0)
+			     :background (my/color-value 'fg)))
+  :config
+  (setq telega-emoji-use-images nil)
+  (general-define-key
+   :keymaps '(telega-root-mode-map telega-chat-mode-map)
+   :states '(normal)
+   "gp" telega-prefix-map)
+  (general-define-key
+   :keymaps '(telega-msg-button-map)
+   "<SPC>" nil)
+  (general-define-key
+   :keymaps '(telega-chat-mode-map)
+   "C-<return>" #'newline)
+  (my/persp-add-rule
+    telega-root-mode 3 "telega"
+    telega-chat-mode 3 "telega"
+    telega-image-mode 3 "telega"
+    telega-webpage-mode 3 "telega"))
+

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.

+
(defun my/telega-server-build ()
+  (interactive)
+  (setq telega-server-libs-prefix
+	(string-trim
+	 (shell-command-to-string "guix build tdlib-1.8.16")))
+  (telega-server-build "CC=gcc"))
+

Setting up the modeline. The default mode string doesn’t look great with my other modeline modules, so I override that.

+
(add-hook 'telega-load-hook #'telega-mode-line-mode)
+(setq telega-mode-line-string-format
+      '("["
+	(:eval
+	 (telega-mode-line-online-status))
+	(:eval
+	 (when telega-use-tracking-for
+	   (telega-mode-line-tracking)))
+	(:eval
+	 (telega-mode-line-unread-unmuted))
+	(:eval
+	 (telega-mode-line-mentions 'messages))
+	"]"))
+

Configuring company backends for the chat buffer, as recommended in the manual:

+
(defun my/telega-chat-setup ()
+  (set (make-local-variable 'company-backends)
+       (append (list telega-emoji-company-backend
+		     'telega-company-username
+		     'telega-company-hashtag
+		     'telega-company-markdown-precode)
+	       (when (telega-chat-bot-p telega-chatbuf--chat)
+		 '(telega-company-botcmd))))
+  (company-mode 1))
+(add-hook 'telega-chat-mode-hook #'my/telega-chat-setup)
+

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.

+
(defun my/telega-online-status ()
+  (derived-mode-p 'telega-root-mode 'telega-chat-mode
+		  'telega-image-mode 'telega-webpage-mode))
+
+(setq telega-online-status-function #'my/telega-online-status)
+

Switch to topic in forum chats.

+
(defun my/telega-switch-to-topic ()
+  (interactive)
+  (let* ((topics-data (gethash
+		       (plist-get telega-chatbuf--chat :id)
+		       telega--chat-topics))
+	 (topics-string
+	  (mapcar
+	   (lambda (topic)
+	     (let* ((name (plist-get (plist-get topic :info) :name))
+		    (unread-count (plist-get topic :unread_count))
+		    (name-string (with-temp-buffer
+				   (telega-ins--topic-title topic 'with-icon)
+				   (buffer-string))))
+	       (if (zerop unread-count)
+		   name-string
+		 (format "%-40s (%s)"
+			 name-string
+			 (propertize (format "%d" unread-count)
+				     'face 'telega-unread-unmuted-modeline)))))
+	   topics-data))
+	 (topics-collection (cl-loop for datum in topics-data
+				     for string in topics-string
+				     collect (cons string datum)))
+	 (topic (completing-read "Topic: " topics-collection nil t)))
+    (telega-chat--goto-thread
+     telega-chatbuf--chat
+     (plist-get
+      (plist-get
+       (alist-get topic topics-collection nil nil #'equal)
+       :info)
+      :message_thread_id))))
+
+(with-eval-after-load 'telega
+  (general-define-key
+   :states '(normal)
+   :keymaps 'telega-chat-mode-map
+   "T" #'my/telega-switch-to-topic))
+

Google Translate

+

Emacs interface to Google Translate.

+

Can’t make it load lazily for some strange reason.

+

References:

+ + +
(use-package google-translate
+  :straight t
+  :if (not my/remote-server)
+  :functions (my-google-translate-at-point google-translate--search-tkk)
+  :custom
+  (google-translate-backend-method 'curl)
+  :config
+  (require 'facemenu)
+  (defun google-translate--search-tkk ()
+    "Search TKK."
+    (list 430675 2721866130))
+  (defun my-google-translate-at-point()
+    "reverse translate if prefix"
+    (interactive)
+    (if current-prefix-arg
+	(google-translate-at-point)
+      (google-translate-at-point-reverse)))
+  (setq google-translate-translation-directions-alist
+	'(("en" . "ru")
+	  ("ru" . "en"))))
+
+(my-leader-def
+  :infix "at"
+  "" '(:which-key "google translate")
+  "p" 'google-translate-at-point
+  "P" 'google-translate-at-point-reverse
+  "q" 'google-translate-query-translate
+  "Q" 'google-translate-query-translate-reverse
+  "t" 'google-translate-smooth-translate)
+

biome

+

biome is my open-meteo client.

+
(use-package biome
+  :straight t
+  :commands (biome)
+  :init
+  (my-leader-def "ab" #'biome)
+  :config
+  (add-to-list 'biome-query-coords
+	       '("Saint-Petersburg, Russia" 59.93863 30.31413))
+  (add-to-list 'biome-query-coords
+	       '("Tyumen, Russia" 57.15222 65.52722)))
+

Reading documentation

+

tldr

+

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.

+
(use-package tldr
+  :straight t
+  :commands (tldr)
+  :config
+  (setq tldr-source-zip-url "https://github.com/tldr-pages/tldr/archive/refs/heads/main.zip")
+
+  (defun tldr-update-docs ()
+    (interactive)
+    (shell-command-to-string (format "curl -L %s --output %s" tldr-source-zip-url tldr-saved-zip-path))
+    (when (file-exists-p "/tmp/tldr")
+      (delete-directory "/tmp/tldr" t))
+    (shell-command-to-string (format "unzip -d /tmp/tldr/ %s" tldr-saved-zip-path))
+    (when (file-exists-p tldr-directory-path)
+      (delete-directory tldr-directory-path 'recursive 'no-trash))
+    (shell-command-to-string (format "mv %s %s" "/tmp/tldr/tldr-main" tldr-directory-path))))
+
+(my-leader-def "hT" 'tldr)
+

man & info

+

Of course, Emacs can also display man and info pages.

+
(setq Man-width-max 180)
+(my-leader-def "hM" 'woman)
+(setq woman-fill-column 90)
+
+(general-define-key
+ :states '(normal)
+ :keymaps 'Info-mode-map
+ (kbd "RET") #'Info-follow-nearest-node
+ "H" #'Info-history-back
+ "L" #'Info-history-forward
+ "n" #'Info-search-next
+ "b" #'Info-search-backward
+ "f" #'ace-link-info)
+
+(defun my/man-fix-width (&rest _)
+  (setq-local Man-width (- (window-width) 4)))
+
+(advice-add #'Man-update-manpage :before #'my/man-fix-width)
+

devdocs.io

+

There is a package called devdocs that does more or less the same, but I like devdocs-browser more because it uses eww.

+
(use-package devdocs-browser
+  :straight t
+  :init
+  (my-leader-def
+    :infix "hd"
+    "" '(:wk "devdocs")
+    "d" #'devdocs-browser-open
+    "o" #'devdocs-browser-open-in
+    "i" #'devdocs-browser-install-doc
+    "n" #'devdocs-browser-uninstall-doc
+    "o" #'devdocs-browser-download-offline-data
+    "O" #'devdocs-browser-remove-offline-data
+    "u" #'devdocs-browser-upgrade-all-docs
+    "r" #'devdocs-browser-update-docs))
+

StackExchange

+

sx.el is a StackExchange client for Emacs.

+
(use-package sx
+  :straight t
+  :config
+  (general-define-key
+   :states '(normal)
+   :keymaps '(sx-question-mode-map sx-question-list-mode-map)
+   "go" #'sx-visit-externally
+   "q" #'quit-window
+   "s*" #'sx-tab-starred
+   "sU" #'sx-tab-unanswered-my-tags
+   "sa" #'sx-ask
+   "sf" #'sx-tab-featured
+   "sh" #'sx-tab-frontpage
+   "si" #'sx-inbox
+   "sm" #'sx-tab-meta-or-main
+   "sn" #'sx-tab-newest
+   "su" #'sx-tab-unanswered
+   "sv" #'sx-tab-topvoted
+   "sw" #'sx-tab-week
+   "u" #'sx-upvote
+   "d" #'sx-downvote
+   "j" nil
+   "k" nil)
+  (general-define-key
+   :states '(normal)
+   :keymaps '(sx-question-mode-map)
+   "gr" #'sx-question-mode-refresh
+   "J" #'sx-question-mode-next-section
+   "K" #'sx-question-mode-previous-section
+   "a" #'sx-answer
+   "e" #'sx-edit
+   "D" #'sx-delete
+   "c" #'sx-comment)
+  (general-define-key
+   :states '(normal)
+   :keymaps '(sx-question-list-mode-map)
+   "RET" 'sx-display
+   "j" #'sx-question-list-next
+   "k" #'sx-question-list-previous
+   "S" #'sx-search
+   "m" #'sx-question-list-mark-read
+   "O" #'sx-question-list-order-by
+   "t" #'sx-tab-switch)
+  (my-leader-def
+   "hs" #'sx-search
+   "hS" #'sx-tab-frontpage)
+  (my/use-colors
+   (sx-question-mode-accepted :foreground (my/color-value 'green)
+			      :weight 'bold)
+   (sx-question-mode-content :background nil))
+  (add-hook 'sx-question-mode-hook #'doom-modeline-mode)
+  (add-hook 'sx-question-list-mode-hook #'doom-modeline-mode))
+

Declarative filesystem management

+

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 first digit is the “category”;
  • +
  • the second digit is the “area”;
  • +
  • the last two digits are the ID.
  • +
+

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:

+
    +
  • git;
  • +
  • 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.

+

Implementation

+
Dependencies
+

We’ll a package called ini.el to parse INI files.

+
(use-package ini
+  :straight (:host github :repo "daniel-ness/ini.el"))
+

The rest is built into Emacs.

+

Org tree

+
Tree definitions
+

The root is my $HOME directory.

+
(defvar my/index-root (concat (getenv "HOME") "/"))
+

The org tree is located in my org-mode folder in a file called index.org:

+
(defvar my/index-file
+  (concat org-directory "/misc/index.org"))
+

Each “area” is an Org header with the folder tag; the Org hierarchy forms the file tree. A header can have the following properties:

+
    +
  • machine - a list of hostnames for which the node is active (or nil)
  • +
  • kind - mega, git, or dummy
  • +
  • remote - remote URL for git
  • +
  • symlink - in case the folder has to be symlinked somewhere else
  • +
+

E.g. a part of the tree above:

+
* 10-19 Code                                                         :folder:
+** 10 [REDACTED]
+*** 10.03 Digital Trajectories
+:PROPERTIES:
+:machine:  indigo eminence
+:project:  t
+:END:
+**** 10.03.A Artifacts
+:PROPERTIES:
+:kind:     mega
+:END:
+**** 10.03.D Documents
+:PROPERTIES:
+:kind:     mega
+:END:
+**** 10.03.R Repos
+***** 10.03.R.00 digital-trajectories-deploy
+:PROPERTIES:
+:kind:     mega
+:END:
+***** 10.03.R.01 digital-trajectories-backend
+:PROPERTIES:
+:kind:     git
+:remote:   [REACTED]
+:END:
+
+**** 10.03.U Dumps
+:PROPERTIES:
+:kind:     dummy
+:END:
+
Parse tree
+

So, let’s parse the Org tree. This is done by recursively traversing the tree returned by org-element-parse-buffer.

+
(defun my/index--tree-get-recursive (heading &optional path)
+  "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-type heading) 'headline)
+    (let (val
+	  (new-path (concat
+		     (or path my/index-root)
+		     (org-element-property :raw-value heading)
+		     "/")))
+      (when-let* ((children (thread-last
+			      (org-element-contents heading)
+			      (mapcar (lambda (e)
+					(my/index--tree-get-recursive
+					 e new-path)))
+			      (seq-filter #'identity))))
+	(setf (alist-get :children val) children))
+      (when-let ((machine (org-element-property :MACHINE heading)))
+	(setf (alist-get :machine val) (split-string machine)))
+      (when-let ((symlink (org-element-property :SYMLINK heading)))
+	(setf (alist-get :symlink val) symlink))
+      (when (org-element-property :PROJECT heading)
+	(setf (alist-get :project val) t))
+      (when-let* ((kind-str (org-element-property :KIND heading))
+		  (kind (intern kind-str)))
+	(setf (alist-get :kind val) kind)
+	(when (equal kind 'git)
+	  (let ((remote (org-element-property :REMOTE heading)))
+	    (unless remote
+	      (user-error "No remote for %s" (alist-get :name val)))
+	    (setf (alist-get :remote val) remote))))
+      (setf (alist-get :name val) (org-element-property :raw-value heading)
+	    (alist-get :path val) new-path)
+      val)))
+
+(defun my/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 :level el) 1)
+			   (seq-contains-p
+			    (mapcar #'substring-no-properties (org-element-property :tags el))
+			    "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:

+
(defun my/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 (rx bos (+ (| num alpha "." "-"))) name)
+    (match-string 0 name)))
+

Then, we can recursively verify the numbers:

+
(defun my/tree--verfify-recursive (elem &optional current)
+  "Verify that ELEM is a valid tree element.
+
+CURRENT is the current number or name of the parent element."
+  (let* ((name (alist-get :name elem))
+	 (number (my/index--extact-number name)))
+    (unless number
+      (user-error "Can't find number: %s" name))
+    (cond
+     ((and (listp current) (not (null current)))
+      (unless (seq-some (lambda (cand) (string-prefix-p cand name)) current)
+	(user-error "Name: %s doesn't match: %s" name current)))
+     ((stringp current)
+      (unless (string-prefix-p current name)
+	(user-error "Name: %s doesn't match: %s" name current))))
+    (let ((recur-value
+	   (if (string-match-p (rx (+ num) "-" (+ num)) number)
+	       (let* ((borders (split-string number "-"))
+		      (start (string-to-number (nth 0 borders)))
+		      (end (string-to-number (nth 1 borders))))
+		 (cl-loop for i from start to (1- end) collect (number-to-string i)))
+	     number)))
+      (mapcar (lambda (e) (my/tree--verfify-recursive e recur-value))
+	      (alist-get :children elem))))
+  t)
+
+(defun my/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-recursive tree))
+
Narrow tree
+

Finally, we need to narrow the tree to only leave nodes that are active for the current machine.

+
(defun my/index--tree-narrow-recursive (elem machine)
+  "Remove all children of ELEM that are not active on MACHINE."
+  (unless (when-let ((elem-machines (alist-get :machine elem)))
+	    (not (seq-some (lambda (elem-machine)
+			     (string-equal elem-machine machine))
+			   elem-machines)))
+    (setf (alist-get :children elem)
+	  (seq-filter
+	   #'identity
+	   (mapcar (lambda (e)
+		     (my/index--tree-narrow-recursive e machine))
+		   (alist-get :children elem))))
+    elem))
+
+(defun my/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-recursive elem (system-name)))
+    (copy-tree tree))))
+
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:

+
(defun my/index--filesystem-tree-mapping (full-tree tree &optional active-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 (or active-paths (my/index--tree-get-paths tree))))
+    (cl-loop for elem in full-tree
+	     for path = (alist-get :path elem)
+	     for extra-folders = (when (and (alist-get :children elem)
+					    (file-directory-p path))
+				   (seq-difference
+				    (mapcar (lambda (d) (if (file-directory-p d)
+							    (concat d "/")
+							  d))
+					    (directory-files path t (rx (not ".") eos)))
+				    (cl-loop for child in (alist-get :children elem)
+					     collect (alist-get :path child))))
+	     for folder-exists = (file-directory-p path)
+	     for folder-has-to-exist = (seq-contains-p active-paths path)
+	     collect `((path . ,path)
+		       (exists . ,folder-exists)
+		       (has-to-exist . ,folder-has-to-exist)
+		       (children . ,(append
+				     (cl-loop for f in extra-folders
+					      collect `((path . ,f)
+							(exists . t)
+							(has-to-exist . nil)
+							(extra . t)))
+				     (my/index--filesystem-tree-mapping
+				      (alist-get :children elem) tree active-paths)))))))
+

And generate commands from the results of the above:

+
(defun my/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-loop for elem in mapping
+	   for path = (alist-get 'path elem)
+	   for exists = (alist-get 'exists elem)
+	   for has-to-exist = (alist-get 'has-to-exist elem)
+	   for extra = (alist-get 'extra elem)
+	   when (and (not exists) has-to-exist)
+	   collect (list (format "mkdir \"%s\"" path) "Make directories" 1)
+	   when (and exists (not has-to-exist))
+	   collect (list (format "rm -rf \"%s\"" path)
+			 (if extra "Remove extra files" "Remove directories")
+			 (if extra 20 10))
+	   append (my/index--filesystem-commands (alist-get 'children elem))))
+
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.

+
(defun my/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-p s)
+				      (string-match-p (rx bos "[" (* nonl) "]") s)
+				      (string-match-p (rx bos (>= 3 " ")) s))))
+		 (split-string string "\n")))
+	 (first-line (car lines))
+	 (headers (split-string first-line))
+	 (header-indices (mapcar
+			  (lambda (header)
+			    (cl-search header first-line))
+			  headers)))
+    (cl-loop for line in (cdr lines)
+	     collect (cl-loop for header in headers
+			      for start in header-indices
+			      for end in (append (cdr header-indices)
+						 (list (length line)))
+			      collect (cons
+				       (intern header)
+				       (string-trim
+					(substring line start end)))))))
+

Now we can invoke mega-sync to get the current sync status. --path-display-size=10000 disables truncation of long paths.

+
(defun my/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-loop for value in mega-result
+	     for localpath = (alist-get 'LOCALPATH value)
+	     collect `((path . ,(if (file-directory-p localpath)
+				    (concat localpath "/")
+				  localpath))
+		       (enabled . ,(string-equal (alist-get 'ACTIVE value)
+						 "Enabled"))))))
+

And get the same data from the tree.

+
(defun my/index--tree-get-paths (tree &optional kind)
+  "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-loop for elem in tree
+	   when (or (null kind) (eq (alist-get :kind elem) kind))
+	   collect (alist-get :path elem)
+	   append (my/index--tree-get-paths
+		   (alist-get :children elem) kind)))
+

With that information, we can generate commands to synchronize the required and actual sync paths.

+
(defun my/index--mega-local-path (path)
+  "Get path in the MEGA cloud by the local path PATH."
+  (string-replace my/index-root "/" path))
+
+(defun my/index--mega-commands (full-tree tree)
+  "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-paths full-tree))
+	 (mega-paths-to-enable (my/index--tree-get-paths tree 'mega))
+	 (mega-info (my/index--mega-data-from-sync))
+	 (mega-paths-enabled (seq-map
+			      (lambda (e) (alist-get 'path e))
+			      (seq-filter (lambda (e) (alist-get 'enabled e))
+					  mega-info)))
+	 (mega-paths-disabled (seq-map
+			       (lambda (e) (alist-get 'path e))
+			       (seq-filter (lambda (e) (not (alist-get 'enabled e)))
+					   mega-info))))
+    (append
+     (cl-loop for path in (seq-difference mega-paths-to-enable mega-paths-enabled)
+	      if (seq-contains-p mega-paths-disabled path)
+	      collect (list (format "mega-sync -e \"%s\"" path) "Mega enable sync" 5)
+	      else append (list
+			   (list (format "mega-mkdir -p \"%s\""
+					 (my/index--mega-local-path path))
+				 "Mega mkdirs" 4)
+			   (list (format "mega-sync \"%s\" \"%s\""
+					 path (my/index--mega-local-path path))
+				 "Mega add sync" 5)))
+     (cl-loop for path in (seq-difference
+			   (seq-intersection mega-paths-enabled paths-all)
+			   mega-paths-to-enable)
+	      collect (list
+		       (format "mega-sync -d \"%s\""
+			       (substring path 0 (1- (length path))))
+		       "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.

+
(defun my/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-loop for elem in tree
+	   for path = (alist-get :path elem)
+	   when (and (eq (alist-get :kind elem) 'git)
+		     (or (not (file-directory-p path))
+			 (directory-empty-p path)))
+	   collect (list (format "git clone \"%s\" \"%s\""
+				 (alist-get :remote elem)
+				 path)
+			 "Init git repos" 2)
+	   append (my/index--git-commands (alist-get :children elem))))
+
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.

+
(defun my/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
+   (rx bos (+ (| num alpha "." "-")) space) "" name))
+
+(defun my/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 ")") "\\\\)")))
+
+(defun my/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-loop for elem in tree
+	   for name = (alist-get :name elem)
+	   if (eq (alist-get :kind elem) 'git)
+	   collect (cons (my/index--wakatime-escape name)
+			 (my/index--wakatime-escape
+			  (my/index--bare-project-name name)))
+	   if (and (eq (alist-get :kind elem) 'git)
+		   (alist-get :symlink elem))
+	   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 :symlink elem)))))
+			 (my/index--wakatime-escape
+			  (my/index--bare-project-name name)))
+	   append (my/index--wakatime-get-map-tree (alist-get :children elem))))
+

And insert that in wakatime.cfg if necessary.

+
(defun my/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-tree tree))
+	 (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-quote map-tree-encoding)
+					   (buffer-string)))))
+    (unless map-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
+
+

The last part here is creating symbolic links.

+
(defun my/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-loop for elem in tree
+	   for path = (alist-get :path elem)
+	   for symlink = (alist-get :symlink elem)
+	   when (and symlink (not (string-match-p (rx "/" eos) symlink)))
+	   do (user-error "Wrong symlink: %s (should be a directory)" symlink)
+	   when (and path symlink
+		     (or (file-exists-p symlink)
+			 (file-exists-p (substring symlink 0 -1)))
+		     (not (file-symlink-p (substring symlink 0 -1))))
+	   collect (list (format "rm -rf %s" (substring symlink 0 -1))
+			 "Remove files to make symlinks" 6)
+	   when (and path symlink
+		     (not (file-symlink-p (substring symlink 0 -1))))
+	   collect (list (format "ln -s '%s' '%s'" path
+				 (substring symlink 0 -1))
+			 "Make symlinks" 7)
+	   append (my/index-get-symlink-commands (alist-get :children elem))))
+
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-local my/index-commands nil
+  "Commands to be executed by `my/index-commands-exec'")
+
+(defun my/index--commands-display (commands)
+  "Display COMMANDS in a buffer.
+
+COMMANDS is a list of commands as defined by `my/index--commands-display'."
+  (unless commands
+    (user-error "No commands to display"))
+  (let ((buffer (get-buffer-create "*index commands*"))
+	(groups (seq-sort-by
+		 (lambda (g) (nth 2 (nth 1 g)))
+		 #'<
+		 (seq-group-by (lambda (c) (nth 1 c))
+			       commands))))
+    (with-current-buffer buffer
+      (sh-mode)
+      (let ((inhibit-read-only t)
+	    commands-sequence)
+	(erase-buffer)
+	(setq-local my/index-commands nil)
+	(cl-loop for g in groups
+		 for group-name = (car g)
+		 for elems = (cdr g)
+		 do (insert "# " group-name "\n")
+		 do (cl-loop for elem in elems
+			     do (push (nth 0 elem) my/index-commands)
+			     do (insert (nth 0 elem) "\n")))
+	(setq-local buffer-read-only t)))
+    (switch-to-buffer buffer)))
+

In order to execute these commands, compile with bash -x on a temporary file is quite sufficient.

+
(defun my/index-commands-exec ()
+  (interactive)
+  (unless (eq major-mode 'sh-mode)
+    (user-error "Not shell mode"))
+  (let ((filename (make-temp-file "index-commands-")))
+    (write-region (point-min) (point-max) filename)
+    (compile (concat "bash -x " filename))))
+

I’ll also try to save some time by caching the resulting index tree. file-has-changed-p is pretty helpful in that.

+
(defvar my/index--tree nil
+  "The last version of the index tree.")
+
+(defun my/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 (null my/index--tree)
+	      (file-has-changed-p my/index-file 'index))
+	  (with-temp-buffer
+	    (insert-file-contents my/index-file)
+	    (let ((buffer-file-name my/index-file))
+	      (my/index--tree-get))))
+	 (t my/index--tree))))
+

With that, we can make the main entrypoint.

+
(defun my/index-commands-sync ()
+  "Sync the filesystem with the index."
+  (interactive)
+  (let* ((full-tree (my/index--tree-retrive)))
+    (my/index--tree-verify full-tree)
+    (let* ((tree (my/index--tree-narrow full-tree))
+	   (mega-commands (my/index--mega-commands full-tree tree))
+	   (mapping (my/index--filesystem-tree-mapping full-tree tree))
+	   (folder-commands (my/index--filesystem-commands mapping))
+	   (git-commands (my/index--git-commands tree))
+	   (waka-commands (my/index--wakatime-commands tree))
+	   (symlink-commands (my/index-get-symlink-commands tree)))
+      (my/index--commands-display (append mega-commands folder-commands git-commands
+					  waka-commands symlink-commands)))))
+
+

The last piece is the navigation interface.

+

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.

+ +

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:

+
(defun my/index--nav-extend (name path)
+  "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-p path)
+    (let* ((number (my/index--extact-number name))
+	   (files (mapcar
+		   (lambda (f) (cons f (concat path f)))
+		   (seq-filter (lambda (f) (not (string-prefix-p "." f)))
+			       (directory-files path))))
+	   (matching-files
+	    (seq-filter
+	     (lambda (f) (and (file-directory-p (cdr f))
+			      (string-prefix-p number (car f))))
+	     files)))
+      (when (and (length> matching-files 0)
+		 (length< matching-files (length files)))
+	(user-error "Extraneuous files in %s" path))
+      (cl-loop for (name-1 . path-1) in matching-files
+	       append (if-let ((child-files (my/index--nav-extend name-1 (concat path-1 "/"))))
+			  (mapcar
+			   (lambda (child-datum)
+			     (push name-1 (alist-get :names child-datum))
+			     child-datum)
+			   child-files)
+			`(((:names . (,name-1))
+			   (:path . ,(concat path-1 "/")))))))))
+

And one to get the navigation data structure.

+
(defun my/index--nav-get (tree &optional names)
+  "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 :path item))
+   #'string-lessp
+   (cl-reduce
+    (lambda (acc elem)
+      (let* ((name (alist-get :name elem))
+	     (path (alist-get :path elem)))
+	(cond ((alist-get :project elem)
+	       (let ((current-nav `((:names . (,@names ,name))
+				    (:path . ,path))))
+		 (when-let (child-navs
+			    (and (alist-get :children elem)
+				 (my/index--nav-get (alist-get :children elem))))
+		   (setf (alist-get :child-navs current-nav) child-navs))
+		 (push current-nav acc)))
+	      ((alist-get :children elem)
+	       (when-let (child-navs (my/index--nav-get
+				      (alist-get :children elem)
+				      `(,@names ,name)))
+		 (cl-loop for child-nav in child-navs
+			  do (push child-nav acc))))
+	      (t (if-let ((extended-nav (my/index--nav-extend name path)))
+		     (cl-loop for child-nav in extended-nav
+			      do (setf (alist-get :names child-nav)
+				       (append names (list name)
+					       (alist-get :names child-nav)))
+			      do (push child-nav acc))
+		   (push `((:names . (,@names ,name))
+			   (:path . ,path))
+			 acc))))
+	acc))
+    tree
+    :initial-value nil)))
+

It also makes sense to cache results of the above.

+
(defvar my/index--nav nil
+  "Navigation stucture for the index.")
+
+(defun my/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 (null my/index--nav)
+	  (file-has-changed-p my/index-file 'nav))
+      (let ((tree (my/index--tree-retrive)))
+	(setq my/index--nav (my/index--nav-get
+			     (my/index--tree-narrow tree))))
+    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.

+
(defun my/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 :names item)))
+			  (alist-get :path item)))
+		  nav))
+	 (ivy-prescient-sort-commands nil))
+    (cdr
+     (assoc
+      (completing-read "Index: " collection nil t)
+      collection))))
+
+(defun my/index--nav-find-path (nav path)
+  "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 :path item) path))
+   nav))
+
+(defun my/index-nav (arg &optional func)
+  "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 (list current-prefix-arg #'dired))
+  (let* ((nav (my/index--nav-retrive))
+	 (current-nav (my/index--nav-find-path
+		       nav (expand-file-name default-directory)))
+	 (current-child-navs (alist-get :child-navs current-nav)))
+    (cond
+     ((or (and (null arg) (null current-child-navs))
+	  (and (equal arg '(4)) current-child-navs))
+      (funcall
+       func
+       (my/index--nav-prompt nav)))
+     ((or (and (equal arg '(4)) (null current-child-navs))
+	  (and (equal arg '(16)) current-child-navs))
+      (let ((selected (my/index--nav-find-path
+		       nav
+		       (my/index--nav-prompt nav))))
+	(if-let (child-navs (alist-get :child-navs selected))
+	    (funcall func (my/index--nav-prompt child-navs))
+	  (funcall func (alist-get :path selected)))))
+     ((and (null arg) current-child-navs)
+      (funcall func (my/index--nav-prompt current-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.

+
(use-package pass
+  :straight t
+  :commands (pass)
+  :init
+  (my-leader-def "ak" #'pass)
+  :config
+  (setq pass-show-keybindings nil))
+

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:

+
(defun my/password-store-get (entry)
+  (if-let ((res (password-store-get entry)))
+      res
+    (my/password-store-get entry)))
+

Docker

+

A package to manage docker containers from Emacs.

+
(use-package docker
+  :straight t
+  :if (not (or my/remote-server my/is-termux))
+  :commands (docker)
+  :init
+  (my-leader-def "ao" 'docker))
+

screenshot.el

+

Tecosaur’s plugin to make beautiful code screenshots.

+ + + + + + + + + + + +
Guix dependency
imagemagick
+
(use-package screenshot
+  :straight (:repo "tecosaur/screenshot"
+		   :host github
+		   :build (:not compile))
+  :if (display-graphic-p)
+  :commands (screenshot)
+  :init
+  (my-leader-def "S" 'screenshot))
+

proced

+

proced is an Emacs built-it process viewer, like top.

+
(my-leader-def "ah" 'proced)
+(setq proced-auto-update-interval 1)
+(add-hook 'proced-mode-hook (lambda ()
+			      (visual-line-mode -1)
+			      (setq-local truncate-lines t)
+			      (proced-toggle-auto-update 1)))
+

Guix

+

An Emacs package to help managing GNU Guix.

+
(use-package guix
+  :straight t
+  :if (not (or my/remote-server my/is-termux))
+  :commands (guix)
+  :init
+  (my-leader-def "ag" 'guix)
+  (defun geiser-company--setup (&rest args)
+    "A dummy function.")
+  (defvar geiser-repl-company-p nil
+    "A dummy variable."))
+

Atomic Chrome

+

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.

+
(use-package atomic-chrome
+  :if (not (or my/remote-server my/is-termux))
+  :commands (atomic-chrome-start-server)
+  :straight t)
+

Pinentry

+

Emacs-based pinentry works great on Termux.

+
(use-package pinentry
+  :straight t
+  :if my/is-termux
+  :config
+  (setenv "GPG_AGENT_INFO" nil) ;; use emacs pinentry
+  (setq auth-source-debug t)
+
+  (setq epg-gpg-program "gpg2") ;; not necessary
+  (require 'epa-file)
+  (epa-file-enable)
+  (setq epa-pinentry-mode 'loopback)
+  (setq epg-pinentry-mode 'loopback)
+  (pinentry-start))
+

Productivity

+

pomm

+

My package for doing Pomodoro timer.

+
(use-package pomm
+  :straight t
+  ;; :straight (:local-repo "~/Code/Emacs/pomm" :files (:defaults "resources"))
+  :commands (pomm pomm-third-time)
+  :init
+  (my-leader-def "ap" #'pomm-third-time)
+  (setq alert-default-style 'libnotify)
+  :config
+  (pomm-mode-line-mode))
+

hledger

+

is a plain-text double-entry accounting software. I use it for managing my personal finances, and thus far it’s great.

+ + + + + + + + + + + +
Guix dependency
hledger
+
(use-package hledger-mode
+  :straight t
+  :mode (rx ".journal" eos)
+  :config
+  (setq hledger-jfile (concat org-directory "/ledger/ledger.journal"))
+  (add-hook 'hledger-mode-hook
+	    (lambda ()
+	      (make-local-variable 'company-backends)
+	      (add-to-list 'company-backends 'hledger-company))))
+
+(use-package flycheck-hledger
+  :straight t
+  :after (hledger-mode))
+

Here are some usage notes.

+

The fastest way to enter new entiries to the journal is by running hledger add

+

Then, run hledger bs to check whether the balance sheet matches the ground truth (e.g. the bank UI).

+

If it doesn’t the simplest way to check for the differences is by running hledger register <item>.

+

Here are some interesting commands to run:

+
    +
  • hledger incomestatement <query>, where <query> is the account prefix. e.g. expenses or revenues. +
      +
    • add --pivot=payee to get grouping by transaction descriptions
    • +
    • add -B to cast currencies
    • +
    +
  • +
+

Calendar

+

Emacs’ built-in calendar. Can even calculate sunrise and sunset times.

+
(setq calendar-date-style 'iso) ;; YYYY/mm/dd
+(setq calendar-week-start-day 1)
+(setq calendar-time-display-form '(24-hours ":" minutes))
+
+(setq calendar-latitude 59.9375)
+(setq calendar-longitude 30.308611)
+

Fun

+

Discord integration

+

Integration with Discord. Shows which file is being edited in Emacs.

+

In order for this to work in Guix, a service is necessary - Discord rich presence.

+

Some functions to override the displayed message:

+
(defun my/elcord-mask-buffer-name (name)
+  (cond
+   ((string-match-p (rx bos (? "CAPTURE-") (= 14 num) "-" (* not-newline) ".org" eos) name)
+    "<ORG-ROAM>")
+   ((string-match-p (rx bos (+ num) "-" (+ num) "-" (+ num) ".org" eos) name)
+    "<ORG-JOURNAL>")
+   ((string-match-p (rx bos "EXWM") name)
+    "<EXWM>")
+   ((string-match-p (rx bos "*Org-Habit") name)
+    "<ORG>")
+   ((with-current-buffer (get-buffer name)
+      (derived-mode-p 'telega-root-mode 'telega-chat-mode))
+    "<TELEGA>")
+   (t name)))
+
+(defun my/elcord-buffer-details-format-functions ()
+  (format "Editing %s" (my/elcord-mask-buffer-name (buffer-name))))
+
+(defun my/elcord-update-presence-mask-advice (r)
+  (list (my/elcord-mask-buffer-name (nth 0 r)) (nth 1 r)))
+

Create a symlink for flatpak:

+
(defun my/elcord-symlink ()
+  (shell-command-to-string "bash -c 'ln -sf {app/com.discordapp.Discord,$XDG_RUNTIME_DIR}/discord-ipc-0 &'"))
+

And the package configuration:

+
(use-package elcord
+  :straight t
+  :if (and (or
+	    (string= (system-name) "indigo")
+	    (string= (system-name) "eminence")
+	    (string= (system-name) "iris"))
+	   (not my/slow-ssh)
+	   (not my/remote-server))
+  :config
+  (setq elcord-buffer-details-format-function #'my/elcord-buffer-details-format-functions)
+  (advice-add 'elcord--try-update-presence :filter-args #'my/elcord-update-presence-mask-advice)
+  (add-to-list 'elcord-mode-text-alist '(telega-chat-mode . "Telega Chat"))
+  (add-to-list 'elcord-mode-text-alist '(telega-root-mode . "Telega Root"))
+  (elcord-mode)
+  (my/elcord-symlink))
+

Snow

+
(use-package snow
+  :straight (:repo "alphapapa/snow.el" :host github)
+  :commands (snow))
+

Power mode

+

When Emacs doesn’t feel powerful enough.

+

Watch out if you are using EXWM.

+
(use-package power-mode
+  :straight (:host github :repo "elizagamedev/power-mode.el")
+  :disabled
+  :commands (power-mode))
+

Redacted

+
(use-package redacted
+  :commands (redacted-mode)
+  :straight (:host github :repo "bkaestner/redacted.el"))
+

Zone

+
(use-package zone
+  :ensure nil
+  :config
+  (setq original-zone-programs (copy-sequence zone-programs)))
+
+(defun my/zone-with-select ()
+  (interactive)
+  (ivy-read "Zone programs"
+	    (cl-pairlis
+	     (cl-mapcar 'symbol-name original-zone-programs)
+	     original-zone-programs)
+	    :action (lambda (elem)
+		      (setq zone-programs (vector (cdr elem)))
+		      (zone))))
+

Gource

+

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:

+
(defun my/gravatar-retrieve-sync (email file-name)
+  "Get gravatar for EMAIL and save it to FILE-NAME."
+  (let ((gravatar-default-image "identicon")
+	(gravatar-size nil)
+	(coding-system-for-write 'binary)
+	(write-region-annotate-functions nil)
+	(write-region-post-annotation-function nil))
+    (write-region
+     (image-property (gravatar-retrieve-synchronously email) :data)
+     nil file-name nil :silent)))
+

To use these images, we need to save them to some folder and use usernames as file names. The folder:

+
(setq my/gravatar-folder "/home/pavel/.cache/gravatars/")
+

And the function that downloads a gravatar if necessary:

+
(defun my/gravatar-save (email author)
+  "Download gravatar for EMAIL.
+
+AUTHOR is the username."
+  (let ((file-name (concat my/gravatar-folder author ".png")))
+    (mkdir my/gravatar-folder t)
+    (unless (file-exists-p file-name)
+      (message "Fetching gravatar for %s (%s)" author email)
+      (my/gravatar-retrieve-sync email file-name))))
+
Merging authors
+

Now to merging authors.

+

Gource itself uses only usernames (without emails), but we can use git log to get both. The required information can be extracted like that:

+
git log --pretty=format:"%ae|%an" | sort | uniq -c | sed "s/^[ \t]*//;s/ /|/"
+

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:

+
(defun my/git-get-authors (repo &optional authors-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-directory repo)
+	 (data (shell-command-to-string
+		"git log --pretty=format:\"%ae|%an\" | sort | uniq -c | sed \"s/^[ \t]*//;s/ /|/\""))
+	 (authors
+	  (cl-loop for string in (split-string data "\n")
+		   if (= (length (split-string string "|")) 3)
+		   collect (let ((datum (split-string string "|")))
+			     `((count . ,(string-to-number (nth 0 datum)))
+			       (email . ,(downcase (nth 1 datum)))
+			       (author . ,(nth 2 datum)))))))
+    (mapcar
+     (lambda (datum)
+       (setf (alist-get 'author datum)
+	     (car (cl-reduce
+		   (lambda (acc author)
+		     (if (> (cdr author) (cdr acc))
+			 author
+		       acc))
+		   (alist-get 'authors datum)
+		   :initial-value '(nil . -1))))
+       (setf (alist-get 'email datum)
+	     (car (cl-reduce
+		   (lambda (acc email)
+		     (if (> (cdr email) (cdr acc))
+			 email
+		       acc))
+		   (alist-get 'emails datum)
+		   :initial-value '(nil . -1))))
+       datum)
+     (cl-reduce
+      (lambda (acc val)
+	(let* ((author (alist-get 'author val))
+	       (email (alist-get 'email val))
+	       (count (alist-get 'count val))
+	       (saved-value
+		(seq-find
+		 (lambda (cand)
+		   (or (alist-get email (alist-get 'emails cand)
+				  nil nil #'string-equal)
+		       (alist-get author (alist-get 'authors cand)
+				  nil nil #'string-equal)
+		       (alist-get email (alist-get 'authors cand)
+				  nil nil #'string-equal)
+		       (alist-get author (alist-get 'emails cand)
+				  nil nil #'string-equal)))
+		 acc)))
+	  (if saved-value
+	      (progn
+		(if (alist-get email (alist-get 'emails saved-value)
+			       nil nil #'string-equal)
+		    (cl-incf (alist-get email (alist-get 'emails saved-value)
+					nil nil #'string-equal)
+			     count)
+		  (push (cons email count) (alist-get 'emails saved-value)))
+		(if (alist-get author (alist-get 'authors saved-value)
+			       nil nil #'string-equal)
+		    (cl-incf (alist-get author (alist-get 'authors saved-value)
+					nil nil #'string-equal)
+			     count)
+		  (push (cons author count) (alist-get 'authors saved-value))))
+	    (setq saved-value
+		  (push `((emails . ((,email . ,count)))
+			  (authors . ((,author . ,count))))
+			acc)))
+	  acc))
+      authors
+      :initial-value authors-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>)
  • +
  • email - the most popular email
  • +
  • author - the most popular username.
  • +
+
Running for multiple repos
+

This section was mostly informed by this page in the gource wiki.

+

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.:

+
1600769568|dsofronov|A|/studentor/.dockerignore
+1600769568|dsofronov|A|/studentor/.editorconfig
+1600769568|dsofronov|A|/studentor/.flake8
+1600769568|dsofronov|A|/studentor/.gitignore
+

where the values of one line are:

+
    +
  • UNIX timestamp
  • +
  • Author name
  • +
  • A for add, M for modify, and D for delete
  • +
  • Path to file
  • +
+

The file has to be sorted by the timestamp in ascending order.

+

So, the function that prepares the log for one repository:

+
(defun my/gource-prepare-log (repo authors)
+  "Create gource log string for REPO.
+
+AUTHORS is the output of `my/git-get-authors'."
+  (let ((log (shell-command-to-string
+	      (concat
+	       "gource --output-custom-log - "
+	       repo)))
+	(authors-mapping (make-hash-table :test #'equal))
+	(prefix (file-name-base repo)))
+    (cl-loop for author-datum in authors
+	     for author = (alist-get 'author author-datum)
+	     do (my/gravatar-save (alist-get 'email author-datum) author)
+	     do (cl-loop for other-author in (alist-get 'authors author-datum)
+			 unless (string-equal (car other-author) author)
+			 do (puthash (car other-author) author
+				     authors-mapping)))
+    (cl-loop for line in (split-string log "\n")
+	     concat (let ((fragments (split-string line "|")))
+		      (when (> (length fragments) 3)
+			(when-let (mapped-author (gethash (nth 1 fragments)
+							  authors-mapping))
+			  (setf (nth 1 fragments) mapped-author))
+			(setf (nth 3 fragments)
+			      (concat "/" prefix (nth 3 fragments))))
+		      (string-join fragments "|"))
+	     concat "\n")))
+

This function:

+
    +
  • Downloads a gravatar for each author
  • +
  • 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:

+
(defun my/gource-dired-create-logs (repos log-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-files nil nil #'file-directory-p)
+			 (user-error "Select at least one directory"))
+		     (read-file-name "Log file name: " nil "combined.log")))
+  (let ((authors
+	 (cl-reduce
+	  (lambda (acc repo)
+	    (my/git-get-authors repo acc))
+	  repos
+	  :initial-value nil)))
+    (with-temp-file log-name
+      (insert
+       (string-join
+	(seq-filter
+	 (lambda (line)
+	   (not (string-empty-p line)))
+	 (seq-sort-by
+	  (lambda (line)
+	    (if-let (time (car (split-string line "|")))
+		(string-to-number time)
+	      0))
+	  #'<
+	  (split-string
+	   (mapconcat
+	    (lambda (repo)
+	      (my/gource-prepare-log repo authors))
+	    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:

+
gource <log-file> --user-image-dir <path-to-gravatars>
+

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.

+

Memes

+

Generate memes from Emacs.

+
(use-package imgur
+  :straight (:host github :repo "larsmagne/imgur.el")
+  :defer t)
+
+(use-package meme
+  :straight (:host github :repo "larsmagne/meme" :files (:defaults "images"))
+  :commands (meme))
+

Guix settings

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Guix dependencyDescription
emacs-vtermThe vterm package
ripgrepA recursive search tool
the-silver-searcherAnother recursive search tool
texinfo
+

+
(when (fboundp #'my/format-guix-dependencies)
+  (my/format-guix-dependencies))
+
(specifications->manifest
+ '("emacs-next-tree-sitter"
+   <<packages()>>))
+
+
+
+
+ Table of Contents + +
+ + +
+
+ +
+ + diff --git a/configs/guix/index.html b/configs/guix/index.html new file mode 100644 index 0000000..8ec5af4 --- /dev/null +++ b/configs/guix/index.html @@ -0,0 +1,887 @@ + + + + + + Guix + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ Guix + +

+
+

+ 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:

+ +

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:

+ +

Activate profiles

+

A script to activate guix profiles. Usage:

+
activate-profiles [profile1] [profile2] ...
+

Source: David Wilson’s config

+
GREEN='\033[1;32m'
+RED='\033[1;30m'
+NC='\033[0m'
+GUIX_EXTRA_PROFILES=$HOME/.guix-extra-profiles
+
+profiles=$*
+if [[ $# -eq 0 ]]; then
+    profiles="$HOME/.config/guix/manifests/*.scm";
+fi
+
+for profile in $profiles; do
+  # Remove the path and file extension, if any
+  profileName=$(basename $profile)
+  profileName="${profileName%.*}"
+  profilePath="$GUIX_EXTRA_PROFILES/$profileName"
+  manifestPath=$HOME/.config/guix/manifests/$profileName.scm
+
+  if [ -f $manifestPath ]; then
+    echo
+    echo -e "${GREEN}Activating profile:" $manifestPath "${NC}"
+    echo
+
+    mkdir -p $profilePath
+    guix package --manifest=$manifestPath --profile="$profilePath/$profileName"
+
+    # Source the new profile
+    GUIX_PROFILE="$profilePath/$profileName"
+    if [ -f $GUIX_PROFILE/etc/profile ]; then
+	. "$GUIX_PROFILE"/etc/profile
+    else
+	echo -e "${RED}Couldn't find profile:" $GUIX_PROFILE/etc/profile "${NC}"
+    fi
+  else
+    echo "No profile found at path" $profilePath
+  fi
+done
+

Update profiles

+

A script to update Guix profiles. Usage:

+
update-profiles [profile1] [profile2] ...
+

Source: once again, David Wilson’s config.

+
GREEN='\033[1;32m'
+NC='\033[0m'
+GUIX_EXTRA_PROFILES=$HOME/.guix-extra-profiles
+
+profiles=$*
+if [[ $# -eq 0 ]]; then
+    profiles="$GUIX_EXTRA_PROFILES/*";
+fi
+
+for profile in $profiles; do
+  profileName=$(basename $profile)
+  profilePath=$GUIX_EXTRA_PROFILES/$profileName
+
+  echo
+  echo -e "${GREEN}Updating profile:" $profilePath "${NC}"
+  echo
+
+  guix package --profile="$profilePath/$profileName" --manifest="$HOME/.config/guix/manifests/$profileName.scm"
+done
+

Run guix package in profile

+
GUIX_EXTRA_PROFILES=$HOME/.guix-extra-profiles
+
+profileName=$(basename $1)
+profileName="${profileName%.*}"
+profilePath="$GUIX_EXTRA_PROFILES/$profileName"
+
+if [ -d $profilePath ]; then
+    guix package --profile="$profilePath/$profileName" ${@:2}
+else
+    echo -e "No profile found at path: " $profilePath
+fi
+

Channels

+

Specifying additional channels.

+

channel-q is my Guix channel. Don’t use it at home.

+

References:

+ + +
(cons*
+ (channel
+  (name 'channel-q)
+  (url "file:///home/pavel/_channel-q"))
+ (channel
+  (name 'flat)
+  (url "https://github.com/flatwhatson/guix-channel.git")
+  (introduction
+   (make-channel-introduction
+    "33f86a4b48205c0dc19d7c036c85393f0766f806"
+    (openpgp-fingerprint
+     "736A C00E 1254 378B A982  7AF6 9DBE 8265 81B6 4490"))))
+ (channel
+  (name 'nonguix)
+  (url "https://gitlab.com/nonguix/nonguix")
+  ;; (commit "d54973e47b89fe5772a5b6e2d0c0b86acb089e27")
+  (introduction
+   (make-channel-introduction
+    "897c1a470da759236cc11798f4e0a5f7d4d59fbc"
+    (openpgp-fingerprint
+     "2A39 3FFF 68F4 EF7A 3D29  12AF 6F51 20A0 22FB B2D5"))))
+ (channel
+  ;; What can possibly go wrong, huh
+  (name 'guix-gaming-games)
+  (url "https://gitlab.com/guix-gaming-channels/games.git")
+  ;; Enable signature verification:
+  (introduction
+   (make-channel-introduction
+    "c23d64f1b8cc086659f8781b27ab6c7314c5cca5"
+    (openpgp-fingerprint
+     "50F3 3E2E 5B0C 3D90 0424  ABE8 9BDC F497 A4BB CC7F"))))
+ %default-channels)
+

Systems

+

Configuring systems with Guix.

+

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
+

Common modules:

+
(use-modules (gnu))
+(use-modules (gnu system nss))
+(use-modules (gnu packages bash))
+(use-modules ((gnu packages base) #:select (coreutils glibc)))
+(use-modules (gnu packages certs))
+(use-modules (gnu packages version-control))
+(use-modules (gnu packages vim))
+(use-modules (gnu packages gnome))
+(use-modules (gnu packages xorg))
+(use-modules (gnu packages wm))
+(use-modules (gnu packages openbox))
+(use-modules (gnu services docker))
+(use-modules (gnu services cups))
+(use-modules (gnu services virtualization))
+(use-modules (srfi srfi-1))
+(use-modules (guix channels))
+(use-modules (guix inferior))
+(use-modules (nongnu packages linux))
+(use-modules (nongnu system linux-initrd))
+
+(use-service-modules desktop networking ssh xorg nix)
+(use-package-modules ssh)
+

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

+
(kernel
+  (let*
+      ((channels
+	(list (channel
+	       (name 'nonguix)
+	       (url "https://gitlab.com/nonguix/nonguix")
+	       (commit "213be7ee6676fc4a3db0e3ac9ce5cd79e2ed209e"))
+	      (channel
+	       (name 'guix)
+	       (url "https://git.savannah.gnu.org/git/guix.git")
+	       (commit "6311493d7a6271bfbc51f4693857f9a12fe9965d"))))
+       (inferior
+	(inferior-for-channels channels)))
+    (first (lookup-inferior-packages inferior "linux" "6.2.9"))))
+;; (kernel linux)
+(initrd microcode-initrd)
+(firmware (list linux-firmware))
+(locale "en_US.utf8")
+(timezone "Europe/Moscow")
+

US/RU keyboard layout, switch with Alt+Shift.

+
(keyboard-layout (keyboard-layout "us,ru" #:options '("grp:alt_shift_toggle")))
+

User accounts.

+
(users (cons* (user-account
+	       (name "pavel")
+	       (comment "Pavel")
+	       (group "users")
+	       (home-directory "/home/pavel")
+	       (supplementary-groups
+		'("wheel"  ;; sudo
+		  "netdev" ;; network devices
+		  "audio"
+		  "video"
+		  "input"
+		  "tty"
+		  "docker"
+		  "scanner"
+		  "libvirt"
+		  "lp")))
+	      %base-user-accounts))
+

Base packages, necessary right after the installation.

+
(packages
+ (append
+  (list nss-certs
+	    git
+	i3-gaps
+	i3lock
+	openbox
+	xterm
+	    vim)
+  %base-packages))
+

Default services for each machine:

+
    +
  • override the default %desktop-services to add OpenVPN support
  • +
  • add nix service
  • +
  • add docker service
  • +
  • add CUPS service
  • +
  • add libvirt service
  • +
  • add a symlink to ELF interpreter to where most Linux binaries expect it
  • +
+ +
(define %my-base-services
+  (cons*
+   (service openssh-service-type)
+   (screen-locker-service i3lock "i3lock")
+   (extra-special-file "/lib64/ld-linux-x86-64.so.2" (file-append glibc "/lib/ld-linux-x86-64.so.2"))
+   (service nix-service-type)
+   (service cups-service-type
+	    (cups-configuration
+	     (web-interface? #t)))
+   (service docker-service-type)
+   (service libvirt-service-type
+	    (libvirt-configuration
+	     (unix-sock-group "libvirt")
+	     (tls-port "16555")))
+   (service virtlog-service-type)
+   (bluetooth-service #:auto-enable? #f)
+   (modify-services %desktop-services
+		    (network-manager-service-type
+		     config =>
+		     (network-manager-configuration
+		      (inherit config)
+		      (vpn-plugins (list network-manager-openvpn))))
+		    (guix-service-type
+		     config =>
+		     (guix-configuration
+		      (inherit config)
+		      (substitute-urls
+		       (append (list "https://substitutes.nonguix.org")
+			       %default-substitute-urls))
+		      (authorized-keys
+		       (append (list (local-file "./signing-key.pub"))
+			       %default-authorized-guix-keys)))))))
+

indigo

+

indigo is my desktop PC.

+
<<system-common>>
+
+(operating-system
+ <<system-base>>
+
+ (host-name "indigo")
+ (services (cons*
+	    (set-xorg-configuration
+	     (xorg-configuration
+	      (keyboard-layout keyboard-layout)))
+	    %my-base-services))
+
+ (bootloader
+  (bootloader-configuration
+   (bootloader grub-efi-bootloader)
+   (target "/boot/efi")
+   (keyboard-layout keyboard-layout)))
+
+ (swap-devices
+  (list (uuid "3a77c542-7d24-46ff-8123-f7398d1c2677")))
+
+ (file-systems
+  (cons* (file-system
+	  (mount-point "/")
+	  (device (file-system-label "my-root"))
+	  (type "ext4"))
+	     (file-system
+	      (mount-point "/boot/efi")
+	      (device "/dev/sda1")
+	      (type "vfat"))
+	 %base-file-systems)))
+

eminence

+

eminence is a HP 15s laptop.

+

%backlight-udev-rule should enable members of video group change the display backlight. See the relevant page at Arch Wiki.

+
<<system-common>>
+
+(define %backlight-udev-rule
+  (udev-rule
+   "90-backlight.rules"
+   (string-append "ACTION==\"add\", SUBSYSTEM==\"backlight\", "
+		  "RUN+=\"/run/current-system/profile/bin/chgrp video /sys/class/backlight/%k/brightness\""
+		  "\n"
+		  "ACTION==\"add\", SUBSYSTEM==\"backlight\", "
+		  "RUN+=\"/run/current-system/profile/bin/chmod g+w /sys/class/backlight/%k/brightness\"")))
+
+(operating-system
+ <<system-base>>
+
+ (host-name "eminence")
+ (services (cons*
+	    (set-xorg-configuration
+	     (xorg-configuration
+	      (keyboard-layout keyboard-layout)))
+	    (modify-services %my-base-services
+			     (elogind-service-type
+			      config =>
+			      (elogind-configuration
+			       (inherit config)
+			       (handle-lid-switch-external-power 'suspend)))
+			     (udev-service-type
+			      config =>
+			      (udev-configuration
+			       (inherit config)
+			       (rules (cons %backlight-udev-rule
+					    (udev-configuration-rules config))))))))
+
+ (bootloader
+  (bootloader-configuration
+   (bootloader grub-efi-bootloader)
+   (target "/boot/efi")
+   (keyboard-layout keyboard-layout)))
+
+ (swap-devices
+  (list (uuid "f93cf3f6-7ee7-42ec-8ee2-f3d896fdf9b5")))
+
+ (file-systems
+  (cons* (file-system
+	  (mount-point "/")
+	  (device
+	   (uuid "1d937704-bbeb-43b5-bc63-453886c426af"
+		 'ext4))
+	  (type "ext4"))
+	 (file-system
+	  (mount-point "/boot/efi")
+	  (device (uuid "0031-3784" 'fat32))
+	  (type "vfat"))
+	 %base-file-systems)))
+

azure

+

azure is a Lenovo Ideapad 330 laptop.

+

%backlight-udev-rule should enable members of video group change the display backlight. See the relevant page at Arch Wiki.

+
<<system-common>>
+
+(define %backlight-udev-rule
+  (udev-rule
+   "90-backlight.rules"
+   (string-append "ACTION==\"add\", SUBSYSTEM==\"backlight\", "
+		  "RUN+=\"/run/current-system/profile/bin/chgrp video /sys/class/backlight/%k/brightness\""
+		  "\n"
+		  "ACTION==\"add\", SUBSYSTEM==\"backlight\", "
+		  "RUN+=\"/run/current-system/profile/bin/chmod g+w /sys/class/backlight/%k/brightness\"")))
+
+(operating-system
+ <<system-base>>
+
+ (host-name "azure")
+ (services (cons*
+	    (set-xorg-configuration
+	     (xorg-configuration
+	      (keyboard-layout keyboard-layout)))
+	    (modify-services %my-base-services
+			     (elogind-service-type config =>
+						   (elogind-configuration (inherit config)
+									  (handle-lid-switch-external-power 'suspend)))
+			     (udev-service-type config =>
+						(udev-configuration (inherit config)
+								    (rules (cons %backlight-udev-rule
+										 (udev-configuration-rules config))))))))
+
+ (bootloader
+  (bootloader-configuration
+   (bootloader grub-efi-bootloader)
+   (target "/boot/efi")
+   (keyboard-layout keyboard-layout)))
+
+ (swap-devices
+  (list (uuid "4b2dedb3-b111-4e69-8c05-6daa2b072c76")))
+
+ (file-systems
+  (cons* (file-system
+	  (mount-point "/")
+	  (device (file-system-label "my-root"))
+	  (type "ext4"))
+	     (file-system
+	      (mount-point "/boot/efi")
+	      (device "/dev/sda1")
+	      (type "vfat"))
+	 %base-file-systems)))
+

iris

+

iris is my work machine.

+
<<system-common>>
+
+(operating-system
+ <<system-base>>
+
+ (host-name "iris")
+ (services (cons*
+	    (set-xorg-configuration
+	     (xorg-configuration
+	      (keyboard-layout keyboard-layout)))
+	    %my-base-services))
+
+ (bootloader (bootloader-configuration
+	      (bootloader grub-bootloader)
+	      (targets (list "/dev/sdb"))
+	      (keyboard-layout keyboard-layout)))
+ (swap-devices (list (swap-space
+		      (target (uuid
+			       "bc284384-ff00-4fbc-abda-1c46f69c0505")))))
+ (mapped-devices (list (mapped-device
+			(source (uuid
+				 "21876acb-e05a-4731-8df0-ba5761910ca8"))
+			(target "cryptroot")
+			(type luks-device-mapping))))
+
+ (file-systems (cons* (file-system
+		       (mount-point "/")
+		       (device "/dev/mapper/cryptroot")
+		       (type "ext4")
+		       (dependencies mapped-devices))
+		      (file-system
+		       (mount-point "/boot/efi")
+		       (device (uuid "782E-F6D3"
+				     'fat32))
+		       (type "vfat")) %base-file-systems)))
+

System installation

+

Preparation

+

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:

+
sudo dd of=/dev/sdxX if=<path-to-iso> status=progress && sync
+

However, I couldn’t make it work for some strange reason. Fortunately, gnome-disk-utility was able to produce a bootable USB.

+

Installation

+

Going further, the official instructions for installation & SystemCrafters wiki entry are pretty good, so it’s not necessary to repeat them here.

+

After installation

+

After the installation, the strategy is as follows.

+

Set a password for the main user (pavel). Login with openbox to get a tolerable interface because i3’s default config is horrible.

+

Connect to the internet.

+

Clone the dotfiles repo:

+
mkdir Code
+cd Code
+git clone https://github.com/SqrtMinusOne/dotfiles.git
+

Copy the channels file and run guix pull:

+
cp ~/Code/dotfiles/.config/guix/channels.scm ~/.config/guix
+guix pull
+

The first pull usually takes a while. After that install yadm and pull dotfiles:

+
guix install yadm
+guix clone https://github.com/SqrtMinusOne/dotfiles.git
+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryGuix dependencyDescription
systempatchelfA program to modify existsing ELF executables
systemglibcA lot of stuff, including ELF interpeter and ldd
systemtor-client
systemtorsocks
systemvnstat
+

OpenVPN

+ + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryGuix dependency
systemopenvpn
systemopenvpn-update-resolve-conf
systemopenresolv
systemvpnc
+

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
    +
  • +
  • +

    added auth-user-pass with a link to login info

    +
    auth-user-pass /home/pavel/.vpn/auth.conf
    +

    auth.conf looks like this:

    +
    login
    +password
    +
  • +
  • +

    Run openvpn-update-resolv-conf script to prevent DNS leaks. openvpn-update-resolve-conf originates in my channel-q.

    +

    Edit <2022-04-07 Thu>: Looks like this doesn’t work on some connections. See the next option in that case

    +
    setenv PATH /home/pavel/.guix-extra-profiles/system/system/bin:/home/pavel/.guix-extra-profiles/system/system/sbin:/home/pavel/.guix-extra-profiles/console/console/bin:/run/current-system/profile/bin:/run/current-system/profile/sbin
    +
    +up /home/pavel/.guix-extra-profiles/system/system/bin/update-resolv-conf.sh
    +down /home/pavel/.guix-extra-profiles/system/system/bin/update-resolv-conf.sh
    +

    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.

    +
  • +
  • +

    Manually fix etc/resolv.conf to prevent DNS leaks

    +
    /home/pavel/.guix-extra-profiles/console/console/bin/cp /etc/resolv.conf /etc/resolv.conf-bak
    +echo "nameserver 8.8.8.8" > /etc/resolv.conf
    +

    Restore resolv.conf

    +
    resolveconf -u
    +
    up /home/pavel/bin/scripts/fix-resolve-conf
    +down /home/pavel/bin/scripts/restore-resolve-conf
    +
  • +
  • +

    run a script to fix Docker routes

    +
    route-up /home/pavel/bin/scripts/vpn-fix-routes
    +

    References:

    + +

    The script itself:

    +
    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.

+
export DISPLAY=: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:

+
    +
  • owned by root
  • +
  • exectuable
  • +
  • not readable by other users
  • +
  • not setuid.
  • +
+

See this answer on StackExchange, and NetworkManager-dispatcher man page.

+

+
echo $(guix build network-manager | grep -ve '-doc$')/bin/nmcli
+

So, here’s the script:

+
#!/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

+

conda mamba 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:

+
export PATH="$HOME/bin/dummies:$PATH"
+mkdir -p ~/.cache/slack
+slack -r ~/.cache/slack
+

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"
+

virt-manager

+

Run the following to fix the network:

+
sudo virsh net-define /run/current-system/profile/etc/libvirt/qemu/networks/default.xml
+sudo virsh net-start default
+sudo herd restart libvirtd
+

wakatime-cli

+ + + + + + + + + + + + + +
NoteDescription
TODOPackage this for Guix
+

Before I figure out how to package this for Guix:

+
    +
  • Clone the repo
  • +
  • Run go build
  • +
  • Copy the binary to the ~/bin folder
  • +
+

Docker

+

Docker Compose plugin v2 isn’t yet available on Guix, but can be installed as follows:

+
curl -SL https://github.com/docker/compose/releases/download/v2.17.2/docker-compose-linux-x86_64 -o $HOME/.docker/cli-plugins/docker-compose
+sudo chmod +x $HOME/.docker/cli-plugins/docker-compose
+

Manifest

+

+
(my/format-guix-dependencies category)
+

System

+
(specifications->manifest
+ '(
+   <<packages("system")>>))
+
+
+ +
+ +
+ + diff --git a/configs/index.html b/configs/index.html new file mode 100644 index 0000000..1f8d895 --- /dev/null +++ b/configs/index.html @@ -0,0 +1,115 @@ + + + + + + Configs + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+

Configs

+ +
+ +
+ + 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 (&gt; quote 0) (concat &#34;\&#34;&#34; color &#34;\&#34;&#34;) color)) (let ((val (if (ct-light-p (my/color-value name)) (my/color-value &#39;black) (my/color-value &#39;white)))) (if (eq quote 1) (concat &#34;\&#34;&#34; val &#34;\&#34;&#34;) val)) (setq-local org-confirm-babel-evaluate nil) .profile Environment export QT_QPA_PLATFORMTHEME=&#34;qt5ct&#34; 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&rsquo;s org renderer ignores TODO status, hence such a prefix. Round brackets instead of square ones to prevent GitHub&rsquo;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&rsquo;t hate one another, no young boy will march to war and I will clean up my Emacs config. But that day isn&rsquo;t today. Me, &lt;2021-05-27 Thu 17:35&gt; in commit 93a0573. Adapted from The Dark Element - &ldquo;The Pallbearer Walks Alone&rdquo;. 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 &ldquo;almost anything&rdquo; 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&rsquo; Guix notes Davil Wilson&rsquo;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 &amp; 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&rsquo; 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.

+

It has a GUI mode, but I prefer headless config.

+
davmail.server=true
+davmail.mode=Auto
+davmail.url=https://mail.etu.ru/owa/
+
+davmail.server.certificate.hash=0C:9E:CF:D3:62:26:DB:FA:F1:EE:36:9D:60:E7:31:71:CF:1F:92:85
+
+davmail.caldavPort=1080
+davmail.imapPort=1143
+davmail.ldapPort=1389
+davmail.popPort=1110
+davmail.smtpPort=1025
+
+davmail.imapAutoExpunge=false
+davmail.enableKeepalive=false
+

Also it’s a bit of problem to get it launched as it looks for its jars in the pwd, so here is a script.

+
cd $HOME/bin/davmail-6.0.0-3375
+./davmail davmail.properties
+

Shepherd service is defined in Desktop.org.

+

OfflineIMAP

+ + + + + + + + + + + +
Guix dependency
offlineimap
+

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
+
[general]
+accounts = pvkorytov
+
+[Account pvkorytov]
+localrepository = pvkorytov-local
+remoterepository = pvkorytov-remote
+
+[Repository pvkorytov-local]
+type = Maildir
+localfolders = ~/Mail/pvkorytov_etu/
+
+[Repository pvkorytov-remote]
+type = IMAP
+remotehost = localhost
+remoteuser = <<mail-username()>>
+remotepass = <<mail-password()>>
+remoteport = 1143
+starttls = no
+ssl = no
+sslcacertfile = /etc/ssl/certs/ca-certificates.crt
+

Notmuch

+ + + + + + + + + + + + + + +
Guix dependency
notmuch
parallel
+

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.

+
[user]
+name=Pavel Korytov
+primary_email=thexcloud@gmail.com
+other_email=progin6304@gmail.com;pvkorytov@etu.ru
+

A list of tags which will be added by notmuch new and directory names which will be ignored by notmuch new.

+
[new]
+tags=new;
+ignore=.osync_workdir;.mbsyncstate;.uidvalidity;.lock;/.*gmailieer\.json.*/
+

Exclude these tags from search by default.

+
[search]
+exclude_tags=trash;spam;
+

Maildir compatibility.

+
[maildir]
+synchronize_flags=true
+

Hooks

+

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:

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
tagfolder
inboxINBOX
sentSent
spamJunk
trashTrash
job.digitalJob_Digital
job.digital.docsJob_Digital.Docs
job.digital.supportJob_Digital.Support
job.digital.superserviceJob_Digital.Superservice
job.digital.applicantsJob_Digital.Applicants
job.moevmJob_Moevm
etuEtu
+

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.

+

+
(setq my/maildir-root "~/Mail")
+
+(let ((rules '()))
+  (dolist (row tags)
+    (let ((tag (nth 0 row))
+	  (folder (nth 1 row)))
+      (unless (string-empty-p make_tag)
+	(add-to-list
+	 'rules
+	 (format "notmuch tag +%s \"path:%s/%s/cur/** AND NOT tag:%s\""
+		 tag root folder tag)
+	 t))
+      (unless (string-empty-p remove)
+	(add-to-list
+	 'rules
+	 (format "notmuch tag -%s \"NOT path:%s/%s/cur/** AND tag:%s AND tag:%s\""
+		 tag root folder tag root_tag)
+	 t))
+      (unless (string-empty-p move)
+	(add-to-list
+	 'rules
+	 (concat
+	  (format "notmuch search --output=files \"NOT path:%s/%s/cur/** AND tag:%s AND tag:%s\""
+		  root folder tag root_tag)
+	  (format " | xargs -I ! mv ! %s/%s/%s/cur/" my/maildir-root root folder))
+	 t))))
+  (unless (string-empty-p archive_root)
+    (add-to-list
+     'rules
+     (concat
+      (format "notmuch search --output=files \"NOT path:%s/%s/cur/** AND %s AND tag:%s\""
+	      root archive_root
+	      (mapconcat
+	       (lambda (row)
+		 (format "NOT tag:%s" (car row)))
+	       tags
+	       " AND ")
+	      root_tag)
+      (format " | xargs -I ! mv ! %s/%s/%s/cur/" my/maildir-root root archive_root))
+     t))
+  (string-join rules "\n"))
+

pre_new

+

This hook runs fetch from Gmail & offlineimap in parallel before the notmuch new command. The parallel command is provided by GNU Parallel.

+

It isn’t necessary to run cd for offlineimap, but it’s easier to write that way.

+

+
(my/mail-format-tags-rules tags "pvkorytov_etu" "pvkorytov" nil nil t "Archive")
+
# GMI="/home/pavel/Programs/miniconda3/envs/mail/bin/gmi"
+GMI="gmi"
+
+echo "Running pre-new filters"
+<<mail-tags(move="t",archive_root="Archive")>>
+echo "Pre-new filters done"
+
+parallel --link -j0 "(cd /home/pavel/Mail/{1}/ && {2} {3})" ::: thexcloud progin6304 pvkorytov_etu ::: "$GMI" "$GMI" "offlineimap" ::: sync sync ""
+

post_new

+

And this hook tags different mailboxes with different tags.

+

+
(my/mail-format-tags-rules tags "pvkorytov_etu" "pvkorytov" t t)
+
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.

+
export DISPLAY=: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:

+
(setq user-mail-address "thexcloud@gmail.com")
+(setq user-full-name "Pavel Korytov")
+

Then, the problem with my Guix setup is that Emacs by default doesn’t see the elisp files of notmuch, so here is a small workaround:

+
(let ((default-directory  "/home/pavel/.guix-extra-profiles/mail/mail/share/emacs/site-lisp"))
+  (normal-top-level-add-subdirs-to-load-path))
+

Some functions to toggle tags:

+
(defun my/notmuch-toggle-trash ()
+  (interactive)
+  (evil-collection-notmuch-toggle-tag "trash" "search" #'ignore))
+
+(defun my/notmuch-toggle-inbox ()
+  (interactive)
+  (evil-collection-notmuch-toggle-tag "inbox" "search" #'ignore))
+
+(defun my/notmuch-toggle-unread ()
+  (interactive)
+  (evil-collection-notmuch-toggle-tag "unread" "search" #'ignore))
+

And notmuch settings:

+
(use-package notmuch
+  ;; :ensure nil
+  :commands (notmuch notmuch-search)
+  :init
+  (my/use-colors
+   (notmuch-wash-cited-text :foreground (doom-color 'yellow)))
+  :config
+  (setq mail-specify-envelope-from t)
+  (setq message-sendmail-envelope-from 'header)
+  (setq mail-envelope-from 'header)
+  (setq notmuch-always-prompt-for-sender t)
+  (setq message-send-mail-function #'message-send-mail-with-sendmail)
+  (setq sendmail-program (executable-find "msmtp"))
+  (setq send-mail-function #'sendmail-send-it)
+  (setq mml-secure-openpgp-sign-with-sender t)
+  (setq notmuch-mua-user-agent-function 'notmuch-mua-user-agent-full)
+  (general-define-key
+   :keymaps 'notmuch-search-mode-map
+   :states '(normal)
+   "d" #'my/notmuch-toggle-trash
+   "i" #'my/notmuch-toggle-inbox
+   "u" #'my/notmuch-toggle-unread)
+  ;; Use org-contacts for completion
+  (require 'org-contacts)
+  (setq notmuch-address-command 'as-is)
+  (add-hook 'notmuch-hello-mode-hook
+	    (lambda () (display-line-numbers-mode 0))))
+

The file is read in init.el.

+

Keybindings

+

I used to have a more complicated keybinding system here, but that seemed to go against the Dao.

+

Root keybindings:

+
(my-leader-def
+  "am" (my/command-in-persp "notmuch" "mail" 0 (notmuch)))
+
(my/persp-add-rule
+  notmuch-hello-mode 0 "mail"
+  notmuch-search-mode 0 "mail"
+  notmuch-tree-mode 0 "mail"
+  notmuch-message-mode 0 "mail")
+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Root tagPrefixKeybinding description
mainmthexcloud@gmail.com
proginpprogin6304@gmail.com
pvkorytovvpvkorytov@etu.ru
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TagPrefixName
inboxiinbox
unreaduunread
sentssent
aall mail
+

The following formats the tables above to a proper syntax for setq notmuch-saved-searches:

+

+
(let ((searches '()))
+  (dolist (root_tag root_tags)
+    (dolist (tag filter_tags)
+      (add-to-list
+       'searches
+       (format "(:name \"%s\" :query \"%s\" :key \"%s\")"
+	       (format "%s (%s)"
+		       (nth 0 root_tag)
+		       (nth 2 tag))
+	       (concat "tag:" (nth 0 root_tag)
+		       (unless (string-empty-p (nth 0 tag))
+			 (concat " AND tag:" (nth 0 tag))))
+	       (concat (nth 1 root_tag) (nth 1 tag)))
+       t)))
+  (string-join searches "\n"))
+
(setq notmuch-saved-searches
+      '((:name "drafts" :query "tag:draft" :key "d")
+	<<format-notmuch-saved-searches()>>))
+
(general-define-key
+ :states '(normal visual)
+ :keymaps '(notmuch-hello-mode-map)
+ "f" #'notmuch-jump-search)
+

Signing messages

+
(with-eval-after-load 'notmuch
+  (add-hook 'message-setup-hook 'mml-secure-sign-pgpmime))
+
+(setq mml-secure-key-preferences
+      '((OpenPGP
+	 (sign
+	  ("thexcloud@gmail.com" "914472A1FD6775C166F96EBEED739ADF81C78160"))
+	 (encrypt))
+	(CMS
+	 (sign)
+	 (encrypt))))
+

Tuning signature

+

By default, message.el inserts the signature at the bottom of the message, like this:

+
<message text>
+
+Person <person@mail.org> writes:
+
+> Stuff
+
+--
+Yours,
+me
+

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:

+
<message text>
+
+--
+Yours,
+me
+
+Person <person@mail.org> writes:
+
+> Stuff
+

So here I modifiy the citation function to insert the signature like in the second example for certain cases.

+

Edit <2022-10-27 Thu>: for consistency’s sake, I’ll make the signature on the top for all cases.

+
(defun my/message-insert-signature-need-on-top ()
+  t)
+

Then advice the notmuch-mua-reply function:

+
(defun my/message-maybe-fix-signature (&rest _)
+  (when (my/message-insert-signature-need-on-top)
+    (save-excursion
+      (goto-char (point-min))
+      (when (re-search-forward message-signature-separator nil t)
+	(move-beginning-of-line 0)
+	(kill-region (point) (point-max)))
+      (message-goto-body)
+      (when (re-search-forward (rx "sign=pgpmime") nil t)
+	(forward-line))
+      (insert (current-kill 0))
+      (insert "\n\n")
+      (set-buffer-modified-p nil))))
+
+(with-eval-after-load 'notmuch-mua
+  (advice-add #'notmuch-mua-reply :after #'my/message-maybe-fix-signature))
+

Warn if no subject

+
(defun my/message-ensure-subject ()
+  (unless (or (message-field-value "Subject")
+	      (y-or-n-p "No subject. Send? "))
+    (user-error "Aborting.")))
+
+(add-hook 'notmuch-mua-send-hook #'my/message-ensure-subject)
+

Capitalize formal pronous

+
(defvar my/ru-formal-pronous
+  '("вы" "вас" "вам" "вами" "ваш" "ваша" "ваше" "ваши" "вашего"
+    "вашей" "вашему" "вашим" "вашем" "вашеми"))
+
+(defvar my/ru-formal-pronous-regex
+  (regexp-opt my/ru-formal-pronous 'words))
+
+(defun my/message-ensure-capitalized-formal-pronouns ()
+  (interactive)
+  (save-excursion
+    (message-goto-body)
+    (cl-block nil
+      (let ((case-fold-search nil)
+	    confirmed)
+	(while (re-search-forward my/ru-formal-pronous-regex nil t)
+	  (let* ((match (match-string 0))
+		 (capitalized (capitalize match))
+		 (beg (match-beginning 0))
+		 (end (match-end 0)))
+	    (if (or confirmed
+		    (y-or-n-p (format "Replace %s with %s? "
+				      match capitalized)))
+		(progn
+		  (delete-region beg end)
+		  (insert capitalized)
+		  (setq confirmed t))
+	      (cl-return))))))))
+
+(add-hook 'notmuch-mua-send-hook #'my/message-ensure-capitalized-formal-pronouns)
+

Ensure password is loaded

+

Otherwise msmtp may call pinentry while Emacs is locked, which means EXWM can’t process the password window.

+
(defun my/ensure-password ()
+  (interactive)
+  (my/password-store-get "Job/Digital/Email/pvkorytov@etu.ru"))
+
+(add-hook 'notmuch-mua-send-hook #'my/ensure-password)
+

mailcap

+

mailcap file is a file which defines how to read to different MIME types. Notmuch also uses it, so why not keep it here.

+
audio/*; mpc add %s
+
+image/*; feh %s
+
+application/msword; /usr/bin/xdg-open %s
+application/pdf; zathura %s
+application/postscript ; zathura %s
+
+text/html; firefox %s
+

Guix settings

+

+
(my/format-guix-dependencies)
+
(specifications->manifest
+ '(
+   <<packages()>>))
+
+
+ +
+ +
+ + diff --git a/configs/readme/index.html b/configs/readme/index.html new file mode 100644 index 0000000..cca3653 --- /dev/null +++ b/configs/readme/index.html @@ -0,0 +1,208 @@ + + + + + + My dotfiles + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ My dotfiles + +

+
+

+ My dotfiles + +

+
+
+ +

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.

+

Table of contents and software:

+ +

(Apparently, links on the second level work only in Emacs 🙁)

+

A few other repositories I may consider a part of my config:

+ +

See also my blog posts.

+

Some statistics

+
+
+ +
+
+ +
+
+ +

Misc

+

Notes

+
    +
  • M-u C-c C-v t to tangle a particular block
  • +
  • M-u M-u C-c C-v t to tangle a particular file
  • +
  • C-c C-v d to demarcate a block
  • +
+

Uses yadm’s post_alt hook to create symlinks

+

Encrypted files

+
Mail/thexcloud/.credentials.gmailieer.json
+Mail/progin6304/.credentials.gmailieer.json
+.emacs.d/private.org
+.emacs.d/private.el
+.emacs.d/.trello/sqrtminusone.el
+.emacs.d/gnus/*
+./.vpn/*
+
+
+ +
+ +
+ + 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 SqrtMinusOne Hugo -- gohugo.io en-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&rsquo;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">&#34;SqrtMinusOne/org-clock-agg&#34;</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 &ldquo;Ungrouped&rdquo;</strong>: Option to include the &ldquo;Ungrouped&rdquo; 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">&#34;Org Agenda + Archive&#34;</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">&#34;.&#34;</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">&#34;/archive/&#34;</span>) <span style="color:#800">t</span>)))))) +</span></span></code></pre></div><p>Note that after updating any of these variables, you&rsquo;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 &ldquo;Custom list&rdquo; in the &ldquo;Files&rdquo; 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] =&gt; 0:28 +</span></span><span style="display:flex;"><span>CLOCK: [2023-12-13 Wed 19:30]--[2023-12-13 Wed 19:40] =&gt; 0:10 +</span></span><span style="display:flex;"><span>:END: +</span></span></code></pre></div><p>And predicates &ldquo;Org file&rdquo;, &ldquo;Day&rdquo;, and &ldquo;Outline path&rdquo;, the records for &ldquo;Thing 1&rdquo; will be processed as follows:</p> +<ul> +<li>&ldquo;Day&rdquo; -&gt; <code>2023-12-13</code></li> +<li>&ldquo;Org file&rdquo; -&gt; <code>tasks.org</code></li> +<li>&ldquo;Outline path&rdquo; -&gt; <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] =&gt; 0:28 : DONE Thing 1 +</span></span><span style="display:flex;"><span>- [2023-12-13 Wed 19:30]--[2023-12-13 Wed 19:40] =&gt; 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">&#39;</span>(<span style="color:#ba2121">&#34;PROJECT_NAME&#34;</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. &quot; %20c %8z&quot; (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 &ldquo;Show records&rdquo; 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] =&gt; %d : %t +</span></span></code></pre></div><h3 id="custom-grouping-predicates-2">Custom grouping predicates</h3> +<p>It&rsquo;s possible to define custom grouping predicates in addition to the default ones. In fact, it&rsquo;s probably the only way to get grouping that is tailored to your particular org workflow; I haven&rsquo;t included my predicates in the package because they aren&rsquo;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">&lt;name&gt;</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">&lt;body&gt;</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&rsquo;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">&#39;</span>((<span style="color:#ba2121">&#34;Events: Offline / Online&#34;</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&rsquo;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 &ldquo;Some meeting&rdquo;, &ldquo;Another meeting&rdquo;, 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">&#34;Event&#34;</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">&#34;meeting&#34;</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">&#34;mt&#34;</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">&#34;offline&#34;</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">&#34;offline&#34;</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">&#34;.&#34;</span>))) +</span></span><span style="display:flex;"><span> <span style="color:#ba2121">&#34;(offline)&#34;</span> +</span></span><span style="display:flex;"><span> (<span style="color:#19177c">seq</span> <span style="color:#ba2121">&#34;[&#34;</span> (<span style="color:#00f">+</span> <span style="color:#19177c">alnum</span>) <span style="color:#ba2121">&#34;]&#34;</span>) )) +</span></span><span style="display:flex;"><span> <span style="color:#ba2121">&#34;&#34;</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">&#34;Meeting&#34;</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">&#39;</span>(<span style="color:#ba2121">&#34;Offline&#34;</span>) <span style="color:#666">&#39;</span>(<span style="color:#ba2121">&#34;Online&#34;</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 &ldquo;Open Meteo Data&rdquo; 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 &ldquo;global&rdquo; variables, such as location, units, etc., and &ldquo;group variables&rdquo;. Groups are usually &ldquo;hourly&rdquo; and &ldquo;daily&rdquo;.</p> +<figure><img src="https://sqrtminusone.xyz/biome-img/query.png"/> +</figure> + +<p>Global variables must always include a location (section &ldquo;Select Coordinates or City&rdquo;). 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">&#39;</span>((<span style="color:#ba2121">&#34;Helsinki, Finland&#34;</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">&#34;Berlin, Germany&#34;</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">&#34;Dubai, UAE&#34;</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 (&ldquo;Settings&rdquo; &gt; &ldquo;Timezone&rdquo;).</p> +<p>The current group is switched with <code>&lt;tab&gt;</code>. Each group&rsquo;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&rsquo;re interested in the meaning of more esoteric ones.</p> +<p>Press <code>RET</code> after you&rsquo;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&rsquo;t support horizontal scrolling, so press <code>c</code> to toggle columns&rsquo; 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">&#34;Weather Forecast&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;hourly&#34;</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">&#34;hourly&#34;</span> <span style="color:#ba2121">&#34;windgusts_10m&#34;</span> <span style="color:#ba2121">&#34;windspeed_10m&#34;</span> <span style="color:#ba2121">&#34;cloudcover&#34;</span> <span style="color:#ba2121">&#34;surface_pressure&#34;</span> <span style="color:#ba2121">&#34;weathercode&#34;</span> <span style="color:#ba2121">&#34;snowfall&#34;</span> <span style="color:#ba2121">&#34;showers&#34;</span> <span style="color:#ba2121">&#34;rain&#34;</span> <span style="color:#ba2121">&#34;relativehumidity_2m&#34;</span> <span style="color:#ba2121">&#34;temperature_2m&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;longitude&#34;</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;latitude&#34;</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">&#39;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&rsquo;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">&#34;Air Quality&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;hourly&#34;</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">&#34;hourly&#34;</span> <span style="color:#ba2121">&#34;uv_index&#34;</span> <span style="color:#ba2121">&#34;european_aqi&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;longitude&#34;</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;latitude&#34;</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">&#34;Weather Forecast&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;hourly&#34;</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">&#34;hourly&#34;</span> <span style="color:#ba2121">&#34;weathercode&#34;</span> <span style="color:#ba2121">&#34;snowfall&#34;</span> <span style="color:#ba2121">&#34;showers&#34;</span> <span style="color:#ba2121">&#34;rain&#34;</span> <span style="color:#ba2121">&#34;temperature_2m&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;longitude&#34;</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;latitude&#34;</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&rsquo;t the most complicated thing I&rsquo;ve done, but it&rsquo;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&rsquo;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&hellip; 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&rsquo;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&rsquo;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&rsquo;s not general enough, namely, it doesn&rsquo;t seem to support specifying <code>:class</code> for child groups&hellip; So I ended up overriding <code>transient--layout</code> in the prefix setup. This doesn&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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">&#34;SqrtMinusOne/reverso.el&#34;</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&rsquo;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 &ldquo;Source language&rdquo; and &ldquo;Target language&rdquo; parameters are self-explanatory. Note that not every language is compatible with every other language in the general case. &ldquo;Swap languages&rdquo; attempts to swap them.</p> +<p>Enabling &ldquo;Brief translation output&rdquo; 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&amp;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 &ldquo;Context results&rdquo; 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 &ldquo;Remove linebreaks&rdquo; 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 &ldquo;errors&rdquo; 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 &ldquo;Information&rdquo; section provides details.</p> +<p>&ldquo;Fix error&rdquo; (<code>f</code> or <code>M-x reverso-check-fix-at-point</code>) opens a completion interface with potential fixes. &ldquo;Ignore error&rdquo; (<code>i</code> or <code>M-x reverso-check-ignore-error</code>) simply removes the overlay and moves to the next error.</p> +<p>&ldquo;Previous error&rdquo; (<code>p</code> or <code>M-x reverso-check-prev-error</code>), &ldquo;Next error&rdquo; (<code>n</code> or <code>M-x reverso-check-next-error</code>), &ldquo;First error&rdquo; (<code>P</code> or <code>M-x reverso-check-first-error</code>) and &ldquo;Last error&rdquo; (<code>L</code> or <code>M-x reverso-check-last-error</code>) serve to navigate the error list.</p> +<p>&ldquo;Clear&rdquo; (<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&rsquo;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&rsquo;ve been using it for over a year, so&hellip; 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&rsquo;re sending lots of automated queries. This is also why I didn&rsquo;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&rsquo;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&rsquo;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">&#39;</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&rsquo;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&rsquo;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&rsquo;t transmit your data to a third-party server you can&rsquo;t control. But it still doesn&rsquo;t like markup syntaxes.</p> +<p>Also, I&rsquo;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&rsquo;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 &gt; Plugins section.</p> +</li> +<li> +<p>Enable &ldquo;Allows accessing this account through the API&rdquo; in the Preferences &gt; Preferences. You also may want to disable &ldquo;Purge unread articles&rdquo;, because elfeed doesn&rsquo;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">&#34;SqrtMinusOne/elfeed-sync&#34;</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&rsquo;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 &ldquo;Sync complete!&rdquo; 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&rsquo;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 &amp; 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 &ldquo;jumping point&rdquo; 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>&hellip;</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 . &lt;group-params&gt;)</code> +Groups are used to group elements under collapsible sections.</li> +<li>Query <code>(query . &lt;query-params&gt;)</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 . &lt;search-params&gt;)</code> +Elfeed search, as defined by <code>elfeed-search-set-filter</code>.</li> +<li>Tags tree <code>(auto-tags . &lt;auto-tags-params&gt;)</code> +A tree generated automatically from the available tags.</li> +<li>Tag groups <code>(tag-groups . &lt;tag-group-params&gt;)</code> +Insert one tag as one group.</li> +<li>a few special forms</li> +</ul> +<p><code>&lt;group-params&gt;</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>&lt;query-params&gt;</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 . &quot;string&quot;)</code> or <code>(title . &lt;form&gt;)</code> +Match feed title with <code>string-match-p</code>. &lt;form&gt; makes sense if you +want to pass something like <code>rx</code>.</li> +<li><code>(author . &quot;string&quot;)</code> or <code>(author . &lt;form&gt;)</code></li> +<li><code>(url . &quot;string&quot;)</code> or <code>(url . &lt;form&gt;)</code></li> +<li><code>(and &lt;q-1&gt; &lt;q-2&gt; ... &lt;q-n&gt;)</code> +Match if all the conditions 1, 2, &hellip;, n match.</li> +<li><code>(or &lt;q-1&gt; &lt;q-2&gt; ... &lt;q-n&gt;)</code> or <code>(&lt;q-1&gt; &lt;q-2&gt; ... &lt;q-n&gt;)</code> +Match if any of the conditions 1, 2, &hellip;, n match.</li> +<li><code>(not &lt;query&gt;)</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 &ldquo;emacs&rdquo; or &ldquo;lisp&rdquo; tags.</li> +<li><code>(and emacs lisp)</code> +Return all feeds that have both &ldquo;emacs&rdquo; and &ldquo;lisp&rdquo; tags.</li> +<li><code>(and (title . &quot;Emacs&quot;) (not planets))</code> +Return all feeds that have &ldquo;Emacs&rdquo; in their title and don&rsquo;t have +the &ldquo;planets&rdquo; tag.</li> +</ul> +<p><code>&lt;search-params&gt;</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>&lt;auto-tags-params&gt;</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 . &lt;query-params&gt;)</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>&lt;tag-group-params&gt;</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 . &lt;query-params&gt;)</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">&#39;</span>((<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;GitHub&#34;</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">&#34;SqrtMinusOne.private.atom&#34;</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">&#34;Guix packages&#34;</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">&#34;Blogs [Software]&#34;</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">&#34;Blogs [People]&#34;</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">&#34;Emacs&#34;</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">&#34;Podcasts&#34;</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">&#34;Videos&#34;</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">&#34;Music&#34;</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">&#34;Tech&#34;</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">&#34;History&#34;</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">&#34;Miscellaneous&#34;</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">&#34;Searches&#34;</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">&#34;@6-months-ago sqrtminusone&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;About me&#34;</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">&#34;+later&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;Check later&#34;</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">&#34;Ungrouped&#34;</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&rsquo;t form this kind of hierarchy in <code>elfeed-feeds</code>, the algorithm will still try to build the most &ldquo;optimal&rdquo; 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&rsquo;t found by other queries.</p> +<p>Passing <code>(:search . (query . &lt;query-params&gt;))</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">&#39;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&rsquo;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">#&#39;</span><span style="color:#19177c">elfeed-update</span> <span style="color:#008000">:override</span> <span style="color:#00f">#&#39;</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">&#34;Filter elfeed search buffer by the feed under the cursor.&#34;</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">&#34;@6-months-ago &#34;</span> +</span></span><span style="display:flex;"><span> <span style="color:#ba2121">&#34;+unread &#34;</span> +</span></span><span style="display:flex;"><span> <span style="color:#ba2121">&#34;=&#34;</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">&#34;?&#34;</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">&#34;&#34;</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&rsquo;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&rsquo;t test this one. It looks similar to this package but seems to require much more fine-tuning, for instance, it doesn&rsquo;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&rsquo;s <a href="https://github.com/NicolasPetton/pass">pass</a>, <code>password-store-ivy</code> is designed as complementary to the Nicolas&rsquo; 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&rsquo;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">&#34;SqrtMinusOne/password-store-ivy&#34;</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&rsquo; 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 &lt;milliseconds&gt;)</code>. Wait for <code>&lt;milliseconds&gt;</code>.</li> +<li><code>(key &lt;key&gt;)</code>. Type <code>&lt;key&gt;</code>.</li> +<li><code>(field &lt;field&gt;)</code>. Type <code>&lt;field&gt;</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">&#39;</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">&#34;username&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#19177c">key</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;Tab&#34;</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">&#34;Return&#34;</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">&#34;username&#34;</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">&#34;url&#34;</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-&lt;name&gt;</code>, where <code>&lt;name&gt;</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>&lt;pass&gt; +</span></span><span style="display:flex;"><span>username: thexcloud@gmail.com +</span></span><span style="display:flex;"><span>url: &lt;url&gt; +</span></span><span style="display:flex;"><span>sequence-autotype: (wait (field . &#34;username&#34;) (key . &#34;Tab&#34;) (key . &#34;Tab&#34;) (field . secret) (key . &#34;Return&#34;)) +</span></span></code></pre></div> + + + A few cases of literate configuration https://sqrtminusone.xyz/posts/2022-02-12-literate/ @@ -2506,6 +3585,180 @@ I&rsquo;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 &ldquo;tag&rdquo; 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&rsquo;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&rsquo;m writing down the progress on a job project, I can leave a tag like <code>job.&lt;project-name&gt;</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 &ldquo;backend&rdquo;, 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:&lt;tag-name&gt;][&lt;tag-description&gt;]] +</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>&lt;tag-name&gt;</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:&lt;tag-name&gt;::&lt;number-of-paragraphs&gt;][&lt;tag-description&gt;]] +</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>+&lt;tag&gt;</code> adds a tag and <code>-&lt;tag&gt;</code> deletes a tag.</p> +<p>If you decide to rename a tag, there&rsquo;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:&lt;kind&gt;:&lt;tag-name&gt;][&lt;tag-description&gt;]] +</span></span><span style="display:flex;"><span>[[org-journal:&lt;kind&gt;:&lt;tag-name&gt;::&lt;number-of-paragraphs&gt;][&lt;tag-description&gt;]] +</span></span></code></pre></div><p>If <code>&lt;kind&gt;</code> is omitted, a tag is considered &ldquo;normal&rdquo;.</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 &lt;2022-04-07 Thu&gt; 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 &ldquo;X&rdquo; 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 &ldquo;All tags&rdquo; 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&rsquo;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&rsquo;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&rsquo;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">&#34;host.%s&#34;</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">&#39;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">&#39;org-journal-after-entry-create-hook</span> +</span></span><span style="display:flex;"><span> <span style="color:#00f">#&#39;</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&rsquo;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">#&#39;</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">&#34;var/org-journal-tags/index.gpg&#34;</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">#&#39;</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&rsquo;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 &amp; <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&rsquo;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&rsquo;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">&#39;exwm-init-hook</span> <span style="color:#00f">#&#39;</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&rsquo;t tested this as thoroughly</del> I haven&rsquo;t run into this issue for nearly a month, so it seems to be fixed. But there&rsquo;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-&lt;index&gt;</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">&#39;</span>((<span style="color:#666">0</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;misc&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#666">1</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;core&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#666">2</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;browser&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#666">3</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;comms&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#666">4</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;dev&#34;</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&rsquo;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">&#34;Firefox&#34;</span> <span style="color:#ba2121">&#34;Nightly&#34;</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">&#34;browser&#34;</span>)) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;Alacritty&#34;</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">&#34;term&#34;</span>)) +</span></span><span style="display:flex;"><span> ((<span style="color:#008000">or</span> <span style="color:#ba2121">&#34;VK&#34;</span> <span style="color:#ba2121">&#34;Slack&#34;</span> <span style="color:#ba2121">&#34;Discord&#34;</span> <span style="color:#ba2121">&#34;TelegramDesktop&#34;</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">&#34;comms&#34;</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">&#39;exwm-manage-finish-hook</span> <span style="color:#00f">#&#39;</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&rsquo;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">&#39;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 &ldquo;s&rdquo; 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 &ldquo;s&rdquo; / <code>M-x pomm-start</code> or stopped competely with &ldquo;S&rdquo; / <code>M-x pomm-stop</code>.</li> +<li><strong>Running</strong>. Can be paused with &ldquo;p&rdquo; / <code>M-x pomm-pause</code> or stopped with &ldquo;S&rdquo; / <code>M-x pomm-stop</code>.</li> +</ul> +<p>The state of the timer can be reset with &ldquo;R&rdquo; or <code>M-x pomm-reset</code>.</p> +<p>&ldquo;u&rdquo; updates the transient buffer. The update is manual because I didn&rsquo;t figure out how to automate this, and I think this is not <em>really</em> necessary.</p> +<p>With &ldquo;r&rdquo; or <code>M-x pomm-set-context</code> you can set the current &ldquo;context&rdquo;, 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 &ldquo;s&rdquo; or <code>M-x pomm-third-time-start</code>.</li> +<li><strong>Running</strong>. Can be stopped with &ldquo;S&rdquo; or <code>M-x pomm-third-time-stop</code>. This resets the accumulated break time.</li> +</ul> +<p>Use &ldquo;b&rdquo; 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">&#39;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>&lt;executable&gt; /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">&#39;pomm-on-tick-hook</span> <span style="color:#19177c">&#39;pomm-update-mode-line-string</span>) +</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">&#39;pomm-on-status-changed-hook</span> <span style="color:#19177c">&#39;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 &gt;&gt; /dev/null; <span style="color:#008000;font-weight:bold">then</span> +</span></span><span style="display:flex;"><span> emacsclient --eval <span style="color:#ba2121">&#34;(if (boundp &#39;pomm-current-mode-line-string) pomm-current-mode-line-string \&#34;\&#34;) &#34;</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">&#34;%F %T&#34;</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&rsquo;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&rsquo;t mean something horrible in some language I don&rsquo;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&rsquo;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 &ldquo;Generate Access Token&rdquo; 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">&#34;My_Online/APIs/genius.com&#34;</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&rsquo;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&rsquo;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&rsquo;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&rsquo; 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 &ldquo;cover_small.jpg&rdquo;, &ldquo;cover_med.jpg&rdquo;, &ldquo;cover_large.jpg&rdquo;.</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">&#39;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&rsquo;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.&lt;extension&gt;</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&rsquo;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.

+ +
+ +
+ +
+ + diff --git a/packages/biome/index.html b/packages/biome/index.html new file mode 100644 index 0000000..f0fa4e3 --- /dev/null +++ b/packages/biome/index.html @@ -0,0 +1,215 @@ + + + + + + BIOME - Bountiful Interface to Open Meteo for Emacs + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ 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-package biome
+  :straight t)
+

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:

+
(setq biome-query-coords
+      '(("Helsinki, Finland" 60.16952 24.93545)
+	("Berlin, Germany" 52.52437 13.41053)
+	("Dubai, UAE" 25.0657 55.17128)))
+

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:

+
(biome-def-preset biome-query-preset-177
+  ((:name . "Weather Forecast")
+   (:group . "hourly")
+   (:params
+    ("hourly" "windgusts_10m" "windspeed_10m" "cloudcover" "surface_pressure" "weathercode" "snowfall" "showers" "rain" "relativehumidity_2m" "temperature_2m")
+    ("longitude" . 24.93545)
+    ("latitude" . 60.16952))))
+

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:

+
(setq biome-grid-format (seq-filter (lambda (f) (not (eq (car-safe (nth 2 f))
+							 'gradient)))
+				    biome-grid-format))
+

Composite queries

+

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:

+
(biome-def-multi-preset biome-query-preset-601
+  (((:name . "Air Quality")
+    (:group . "hourly")
+    (:params
+     ("hourly" "uv_index" "european_aqi")
+     ("longitude" . 24.93545)
+     ("latitude" . 60.16952)))
+   ((:name . "Weather Forecast")
+    (:group . "hourly")
+    (:params
+     ("hourly" "weathercode" "snowfall" "showers" "rain" "temperature_2m")
+     ("longitude" . 24.93545)
+     ("latitude" . 60.16952)))))
+

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.

+ +
+ +
+ +
+ + diff --git a/packages/elfeed-summary/index.html b/packages/elfeed-summary/index.html new file mode 100644 index 0000000..4edffa7 --- /dev/null +++ b/packages/elfeed-summary/index.html @@ -0,0 +1,526 @@ + + + + + + elfeed-summary + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ elfeed-summary + + + +

+
+

+ elfeed-summary + + + +

+
+
+ +

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.

+

Inspired by newsboat.

+
+
+ +

Installation

+

The package is available on MELPA, so install it however you normally install packages. My preferred way is use-package with straight:

+
(use-package elfeed-summary
+  :straight t)
+

Of course, you have to have elfeed configured.

+

Usage

+

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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeybindingCommandDescription
RETelfeed-summary--actionOpen 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-RETelfeed-summary--action-show-readOpen thing under the cursor, but always include read items
qQuit the summary buffer
relfeed-summary--refreshRefresh the summary buffer
Relfeed-summary-updateRun update for elfeed feeds
uelfeed-summary-toggle-only-unreadToggle showing only unread entries
Uelfeed-summary--action-mark-readMark 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:

+
(setq elfeed-summary-settings
+      '((group (:title . "GitHub")
+	       (:elements
+		(query . (url . "SqrtMinusOne.private.atom"))
+		(group . ((:title . "Guix packages")
+			  (:elements
+			   (query . (and github guix_packages)))
+			  (:hide t)))))
+	(group (:title . "Blogs [Software]")
+	       (:elements
+		(query . software_blogs)))
+	(group (:title . "Blogs [People]")
+	       (:elements
+		(query . (and blogs people (not emacs)))
+		(group (:title . "Emacs")
+		       (:elements
+			(query . (and blogs people emacs))))))
+	(group (:title . "Podcasts")
+	       (:elements
+		(query . podcasts)))
+	(group (:title . "Videos")
+	       (:elements
+		(group
+		 (:title . "Music")
+		 (:elements
+		  (query . (and videos music))))
+		(group
+		 (:title . "Tech")
+		 (:elements
+		  (query . (and videos tech))))
+		(group
+		 (:title . "History")
+		 (:elements
+		  (query . (and videos history))))
+		;; ...
+		))
+	;; ...
+	(group (:title . "Miscellaneous")
+	       (:elements
+		(group
+		 (:title . "Searches")
+		 (:elements
+		  (search
+		   (:filter . "@6-months-ago sqrtminusone")
+		   (:title . "About me"))
+		  (search
+		   (:filter . "+later")
+		   (:title . "Check later"))))
+		(group
+		 (:title . "Ungrouped")
+		 (:elements :misc))))))
+

Automatic generation of groups

+

auto-tags

+

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:

+
feed1 tag1 tag2
+feed2 tag1 tag2
+feed3 tag1 tag3
+feed4 tag1 tag3
+

It will create the following tree:

+
    +
  • tag1 +
      +
    • tag2 +
        +
      • feed1
      • +
      • feed2
      • +
      +
    • +
    • tag3 +
        +
      • feed3
      • +
      • feed4
      • +
      +
    • +
    +
  • +
+

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:

+
* tag1                      :tag1:
+** feed1
+** feed2                    :tag2:
+** feed3                    :tag2:
+* tag3                      :tag3:
+** feed4                    :tag2:
+** feed5                    :tag2:
+** feed6                    :tag2:
+

Will be converted to this:

+
    +
  • tag1 +
      +
    • feed1
    • +
    • tag2 +
        +
      • feed2
      • +
      • feed3
      • +
      +
    • +
    +
  • +
  • tag3 +
      +
    • tag2 +
        +
      • feed4
      • +
      • feed5
      • +
      • feed6
      • +
      +
    • +
    +
  • +
+

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:

+
(setq elfeed-summary-other-window t)
+

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:

+
(setq elfeed-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:

+
(advice-add #'elfeed-update :override #'elfeed-summary-update)
+

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:

+
(defun my/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-p entry)
+    (elfeed-search-set-filter
+     (concat
+      "@6-months-ago "
+      "+unread "
+      "="
+      (replace-regexp-in-string
+       (rx "?" (* not-newline) eos)
+       ""
+       (elfeed-feed-url (elfeed-entry-feed entry)))))))
+

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.

+ +
+ +
+ +
+ + diff --git a/packages/elfeed-sync/index.html b/packages/elfeed-sync/index.html new file mode 100644 index 0000000..6ffbd2d --- /dev/null +++ b/packages/elfeed-sync/index.html @@ -0,0 +1,216 @@ + + + + + + elfeed-sync + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ elfeed-sync + + + +

+
+

+ elfeed-sync + + + +

+

Sync read/marked status of entries between elfeed and tt-rss. Supports elfeed-summary.

+

DISCLAIMER: It’s still an alpha version of the package, so you may want to backup your elfeed index and tt-rss database.

+
+
+ +

Installation

+

The project consists of the tt-rss plugin and the Emacs package.

+

If you are using the tt-rss docker setup, the steps are as follows. Change them accordingly if you are not.

+
    +
  1. +

    Mount the /var/www/html directory from the container somewhere to the filesystem as described here.

    +
  2. +
  3. +

    Put the repository to the tt-rss/plugins.local/elfeed_sync folder:

    +
    cd ./html/tt-rss/plugins.local/
    +git clone https://github.com/SqrtMinusOne/elfeed-sync.git elfeed_sync
    +
  4. +
  5. +

    Add elfeed_sync to the TTRSS_PLUGINS environment variable.

    +
    TTRSS_PLUGINS=auth_internal, auth_remote, nginx_xaccel, elfeed_sync
    +
  6. +
  7. +

    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.

    +
  8. +
  9. +

    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.

    +
  10. +
  11. +

    Then restart tt-rss. Check if the plugin appears in the Preferences > Plugins section.

    +
  12. +
  13. +

    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.

    +
  14. +
+

Install the Emacs package however you normally install packages, I prefer use-package and straight.el. Make sure to enable elfeed-sync-mode.

+
(use-package elfeed-sync
+  :straight (:host github :repo "SqrtMinusOne/elfeed-sync")
+  :after elfeed
+  :config
+  (elfeed-sync-mode))
+

Then set up the following variables:

+
    +
  • 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.

+ +
+ +
+ +
+ + diff --git a/packages/exwm-modeline/index.html b/packages/exwm-modeline/index.html new file mode 100644 index 0000000..fc122d8 --- /dev/null +++ b/packages/exwm-modeline/index.html @@ -0,0 +1,158 @@ + + + + + + exwm-modeline + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ exwm-modeline + + + +

+
+

+ exwm-modeline + + + +

+
+
+ +

A modeline segment to display exwm workspaces.

+

Here’s how it looks near the list of perspectives (the segment of the current package is to the left): +

+
    +
  • workspaces 0 and 5 do not have any X windows
  • +
  • workspace 1 is the current workspace
  • +
  • workspace 2 has at least one X window.
  • +
+

Features:

+
    +
  • Supports exwm-randr to display only workspaces related to the current monitor.
  • +
  • Numbers are clickable.
  • +
+

Installation

+

The package is available on MELPA. Install it however you usually install packages, I use use-package and straight.el:

+
(use-package exwm-modeline
+  :straight t
+  :after (exwm))
+

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.

+

doom-modeline-exwm by @elken also was a source of inspiration.

+ +
+ +
+ +
+ + diff --git a/packages/index.html b/packages/index.html new file mode 100644 index 0000000..72ed579 --- /dev/null +++ b/packages/index.html @@ -0,0 +1,129 @@ + + + + + + Packages + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + 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&rsquo;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">&#34;SqrtMinusOne/org-clock-agg&#34;</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 &ldquo;Ungrouped&rdquo;</strong>: Option to include the &ldquo;Ungrouped&rdquo; 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">&#34;Org Agenda + Archive&#34;</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">&#34;.&#34;</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">&#34;/archive/&#34;</span>) <span style="color:#800">t</span>)))))) +</span></span></code></pre></div><p>Note that after updating any of these variables, you&rsquo;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 &ldquo;Custom list&rdquo; in the &ldquo;Files&rdquo; 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] =&gt; 0:28 +</span></span><span style="display:flex;"><span>CLOCK: [2023-12-13 Wed 19:30]--[2023-12-13 Wed 19:40] =&gt; 0:10 +</span></span><span style="display:flex;"><span>:END: +</span></span></code></pre></div><p>And predicates &ldquo;Org file&rdquo;, &ldquo;Day&rdquo;, and &ldquo;Outline path&rdquo;, the records for &ldquo;Thing 1&rdquo; will be processed as follows:</p> +<ul> +<li>&ldquo;Day&rdquo; -&gt; <code>2023-12-13</code></li> +<li>&ldquo;Org file&rdquo; -&gt; <code>tasks.org</code></li> +<li>&ldquo;Outline path&rdquo; -&gt; <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] =&gt; 0:28 : DONE Thing 1 +</span></span><span style="display:flex;"><span>- [2023-12-13 Wed 19:30]--[2023-12-13 Wed 19:40] =&gt; 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">&#39;</span>(<span style="color:#ba2121">&#34;PROJECT_NAME&#34;</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. &quot; %20c %8z&quot; (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 &ldquo;Show records&rdquo; 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] =&gt; %d : %t +</span></span></code></pre></div><h3 id="custom-grouping-predicates-2">Custom grouping predicates</h3> +<p>It&rsquo;s possible to define custom grouping predicates in addition to the default ones. In fact, it&rsquo;s probably the only way to get grouping that is tailored to your particular org workflow; I haven&rsquo;t included my predicates in the package because they aren&rsquo;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">&lt;name&gt;</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">&lt;body&gt;</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&rsquo;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">&#39;</span>((<span style="color:#ba2121">&#34;Events: Offline / Online&#34;</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&rsquo;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 &ldquo;Some meeting&rdquo;, &ldquo;Another meeting&rdquo;, 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">&#34;Event&#34;</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">&#34;meeting&#34;</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">&#34;mt&#34;</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">&#34;offline&#34;</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">&#34;offline&#34;</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">&#34;.&#34;</span>))) +</span></span><span style="display:flex;"><span> <span style="color:#ba2121">&#34;(offline)&#34;</span> +</span></span><span style="display:flex;"><span> (<span style="color:#19177c">seq</span> <span style="color:#ba2121">&#34;[&#34;</span> (<span style="color:#00f">+</span> <span style="color:#19177c">alnum</span>) <span style="color:#ba2121">&#34;]&#34;</span>) )) +</span></span><span style="display:flex;"><span> <span style="color:#ba2121">&#34;&#34;</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">&#34;Meeting&#34;</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">&#39;</span>(<span style="color:#ba2121">&#34;Offline&#34;</span>) <span style="color:#666">&#39;</span>(<span style="color:#ba2121">&#34;Online&#34;</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 &ldquo;Open Meteo Data&rdquo; 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 &ldquo;global&rdquo; variables, such as location, units, etc., and &ldquo;group variables&rdquo;. Groups are usually &ldquo;hourly&rdquo; and &ldquo;daily&rdquo;.</p> +<figure><img src="https://sqrtminusone.xyz/biome-img/query.png"/> +</figure> + +<p>Global variables must always include a location (section &ldquo;Select Coordinates or City&rdquo;). 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">&#39;</span>((<span style="color:#ba2121">&#34;Helsinki, Finland&#34;</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">&#34;Berlin, Germany&#34;</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">&#34;Dubai, UAE&#34;</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 (&ldquo;Settings&rdquo; &gt; &ldquo;Timezone&rdquo;).</p> +<p>The current group is switched with <code>&lt;tab&gt;</code>. Each group&rsquo;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&rsquo;re interested in the meaning of more esoteric ones.</p> +<p>Press <code>RET</code> after you&rsquo;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&rsquo;t support horizontal scrolling, so press <code>c</code> to toggle columns&rsquo; 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">&#34;Weather Forecast&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;hourly&#34;</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">&#34;hourly&#34;</span> <span style="color:#ba2121">&#34;windgusts_10m&#34;</span> <span style="color:#ba2121">&#34;windspeed_10m&#34;</span> <span style="color:#ba2121">&#34;cloudcover&#34;</span> <span style="color:#ba2121">&#34;surface_pressure&#34;</span> <span style="color:#ba2121">&#34;weathercode&#34;</span> <span style="color:#ba2121">&#34;snowfall&#34;</span> <span style="color:#ba2121">&#34;showers&#34;</span> <span style="color:#ba2121">&#34;rain&#34;</span> <span style="color:#ba2121">&#34;relativehumidity_2m&#34;</span> <span style="color:#ba2121">&#34;temperature_2m&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;longitude&#34;</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;latitude&#34;</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">&#39;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&rsquo;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">&#34;Air Quality&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;hourly&#34;</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">&#34;hourly&#34;</span> <span style="color:#ba2121">&#34;uv_index&#34;</span> <span style="color:#ba2121">&#34;european_aqi&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;longitude&#34;</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;latitude&#34;</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">&#34;Weather Forecast&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;hourly&#34;</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">&#34;hourly&#34;</span> <span style="color:#ba2121">&#34;weathercode&#34;</span> <span style="color:#ba2121">&#34;snowfall&#34;</span> <span style="color:#ba2121">&#34;showers&#34;</span> <span style="color:#ba2121">&#34;rain&#34;</span> <span style="color:#ba2121">&#34;temperature_2m&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;longitude&#34;</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;latitude&#34;</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&rsquo;t the most complicated thing I&rsquo;ve done, but it&rsquo;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&rsquo;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&hellip; 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&rsquo;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&rsquo;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&rsquo;s not general enough, namely, it doesn&rsquo;t seem to support specifying <code>:class</code> for child groups&hellip; So I ended up overriding <code>transient--layout</code> in the prefix setup. This doesn&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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">&#34;SqrtMinusOne/reverso.el&#34;</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&rsquo;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 &ldquo;Source language&rdquo; and &ldquo;Target language&rdquo; parameters are self-explanatory. Note that not every language is compatible with every other language in the general case. &ldquo;Swap languages&rdquo; attempts to swap them.</p> +<p>Enabling &ldquo;Brief translation output&rdquo; 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&amp;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 &ldquo;Context results&rdquo; 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 &ldquo;Remove linebreaks&rdquo; 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 &ldquo;errors&rdquo; 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 &ldquo;Information&rdquo; section provides details.</p> +<p>&ldquo;Fix error&rdquo; (<code>f</code> or <code>M-x reverso-check-fix-at-point</code>) opens a completion interface with potential fixes. &ldquo;Ignore error&rdquo; (<code>i</code> or <code>M-x reverso-check-ignore-error</code>) simply removes the overlay and moves to the next error.</p> +<p>&ldquo;Previous error&rdquo; (<code>p</code> or <code>M-x reverso-check-prev-error</code>), &ldquo;Next error&rdquo; (<code>n</code> or <code>M-x reverso-check-next-error</code>), &ldquo;First error&rdquo; (<code>P</code> or <code>M-x reverso-check-first-error</code>) and &ldquo;Last error&rdquo; (<code>L</code> or <code>M-x reverso-check-last-error</code>) serve to navigate the error list.</p> +<p>&ldquo;Clear&rdquo; (<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&rsquo;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&rsquo;ve been using it for over a year, so&hellip; 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&rsquo;re sending lots of automated queries. This is also why I didn&rsquo;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&rsquo;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&rsquo;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">&#39;</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&rsquo;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&rsquo;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&rsquo;t transmit your data to a third-party server you can&rsquo;t control. But it still doesn&rsquo;t like markup syntaxes.</p> +<p>Also, I&rsquo;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&rsquo;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 &gt; Plugins section.</p> +</li> +<li> +<p>Enable &ldquo;Allows accessing this account through the API&rdquo; in the Preferences &gt; Preferences. You also may want to disable &ldquo;Purge unread articles&rdquo;, because elfeed doesn&rsquo;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">&#34;SqrtMinusOne/elfeed-sync&#34;</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&rsquo;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 &ldquo;Sync complete!&rdquo; 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 &amp; 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 &ldquo;jumping point&rdquo; 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>&hellip;</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 . &lt;group-params&gt;)</code> +Groups are used to group elements under collapsible sections.</li> +<li>Query <code>(query . &lt;query-params&gt;)</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 . &lt;search-params&gt;)</code> +Elfeed search, as defined by <code>elfeed-search-set-filter</code>.</li> +<li>Tags tree <code>(auto-tags . &lt;auto-tags-params&gt;)</code> +A tree generated automatically from the available tags.</li> +<li>Tag groups <code>(tag-groups . &lt;tag-group-params&gt;)</code> +Insert one tag as one group.</li> +<li>a few special forms</li> +</ul> +<p><code>&lt;group-params&gt;</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>&lt;query-params&gt;</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 . &quot;string&quot;)</code> or <code>(title . &lt;form&gt;)</code> +Match feed title with <code>string-match-p</code>. &lt;form&gt; makes sense if you +want to pass something like <code>rx</code>.</li> +<li><code>(author . &quot;string&quot;)</code> or <code>(author . &lt;form&gt;)</code></li> +<li><code>(url . &quot;string&quot;)</code> or <code>(url . &lt;form&gt;)</code></li> +<li><code>(and &lt;q-1&gt; &lt;q-2&gt; ... &lt;q-n&gt;)</code> +Match if all the conditions 1, 2, &hellip;, n match.</li> +<li><code>(or &lt;q-1&gt; &lt;q-2&gt; ... &lt;q-n&gt;)</code> or <code>(&lt;q-1&gt; &lt;q-2&gt; ... &lt;q-n&gt;)</code> +Match if any of the conditions 1, 2, &hellip;, n match.</li> +<li><code>(not &lt;query&gt;)</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 &ldquo;emacs&rdquo; or &ldquo;lisp&rdquo; tags.</li> +<li><code>(and emacs lisp)</code> +Return all feeds that have both &ldquo;emacs&rdquo; and &ldquo;lisp&rdquo; tags.</li> +<li><code>(and (title . &quot;Emacs&quot;) (not planets))</code> +Return all feeds that have &ldquo;Emacs&rdquo; in their title and don&rsquo;t have +the &ldquo;planets&rdquo; tag.</li> +</ul> +<p><code>&lt;search-params&gt;</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>&lt;auto-tags-params&gt;</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 . &lt;query-params&gt;)</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>&lt;tag-group-params&gt;</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 . &lt;query-params&gt;)</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">&#39;</span>((<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;GitHub&#34;</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">&#34;SqrtMinusOne.private.atom&#34;</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">&#34;Guix packages&#34;</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">&#34;Blogs [Software]&#34;</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">&#34;Blogs [People]&#34;</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">&#34;Emacs&#34;</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">&#34;Podcasts&#34;</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">&#34;Videos&#34;</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">&#34;Music&#34;</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">&#34;Tech&#34;</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">&#34;History&#34;</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">&#34;Miscellaneous&#34;</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">&#34;Searches&#34;</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">&#34;@6-months-ago sqrtminusone&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;About me&#34;</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">&#34;+later&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;Check later&#34;</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">&#34;Ungrouped&#34;</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&rsquo;t form this kind of hierarchy in <code>elfeed-feeds</code>, the algorithm will still try to build the most &ldquo;optimal&rdquo; 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&rsquo;t found by other queries.</p> +<p>Passing <code>(:search . (query . &lt;query-params&gt;))</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">&#39;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&rsquo;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">#&#39;</span><span style="color:#19177c">elfeed-update</span> <span style="color:#008000">:override</span> <span style="color:#00f">#&#39;</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">&#34;Filter elfeed search buffer by the feed under the cursor.&#34;</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">&#34;@6-months-ago &#34;</span> +</span></span><span style="display:flex;"><span> <span style="color:#ba2121">&#34;+unread &#34;</span> +</span></span><span style="display:flex;"><span> <span style="color:#ba2121">&#34;=&#34;</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">&#34;?&#34;</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">&#34;&#34;</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&rsquo;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&rsquo;t test this one. It looks similar to this package but seems to require much more fine-tuning, for instance, it doesn&rsquo;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&rsquo;s <a href="https://github.com/NicolasPetton/pass">pass</a>, <code>password-store-ivy</code> is designed as complementary to the Nicolas&rsquo; 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&rsquo;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">&#34;SqrtMinusOne/password-store-ivy&#34;</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&rsquo; 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 &lt;milliseconds&gt;)</code>. Wait for <code>&lt;milliseconds&gt;</code>.</li> +<li><code>(key &lt;key&gt;)</code>. Type <code>&lt;key&gt;</code>.</li> +<li><code>(field &lt;field&gt;)</code>. Type <code>&lt;field&gt;</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">&#39;</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">&#34;username&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#19177c">key</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;Tab&#34;</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">&#34;Return&#34;</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">&#34;username&#34;</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">&#34;url&#34;</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-&lt;name&gt;</code>, where <code>&lt;name&gt;</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>&lt;pass&gt; +</span></span><span style="display:flex;"><span>username: thexcloud@gmail.com +</span></span><span style="display:flex;"><span>url: &lt;url&gt; +</span></span><span style="display:flex;"><span>sequence-autotype: (wait (field . &#34;username&#34;) (key . &#34;Tab&#34;) (key . &#34;Tab&#34;) (field . secret) (key . &#34;Return&#34;)) +</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 &ldquo;tag&rdquo; 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&rsquo;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&rsquo;m writing down the progress on a job project, I can leave a tag like <code>job.&lt;project-name&gt;</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 &ldquo;backend&rdquo;, 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:&lt;tag-name&gt;][&lt;tag-description&gt;]] +</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>&lt;tag-name&gt;</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:&lt;tag-name&gt;::&lt;number-of-paragraphs&gt;][&lt;tag-description&gt;]] +</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>+&lt;tag&gt;</code> adds a tag and <code>-&lt;tag&gt;</code> deletes a tag.</p> +<p>If you decide to rename a tag, there&rsquo;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:&lt;kind&gt;:&lt;tag-name&gt;][&lt;tag-description&gt;]] +</span></span><span style="display:flex;"><span>[[org-journal:&lt;kind&gt;:&lt;tag-name&gt;::&lt;number-of-paragraphs&gt;][&lt;tag-description&gt;]] +</span></span></code></pre></div><p>If <code>&lt;kind&gt;</code> is omitted, a tag is considered &ldquo;normal&rdquo;.</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 &lt;2022-04-07 Thu&gt; 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 &ldquo;X&rdquo; 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 &ldquo;All tags&rdquo; 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&rsquo;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&rsquo;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&rsquo;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">&#34;host.%s&#34;</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">&#39;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">&#39;org-journal-after-entry-create-hook</span> +</span></span><span style="display:flex;"><span> <span style="color:#00f">#&#39;</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&rsquo;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">#&#39;</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">&#34;var/org-journal-tags/index.gpg&#34;</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">#&#39;</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&rsquo;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 &amp; <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&rsquo;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">&#39;exwm-init-hook</span> <span style="color:#00f">#&#39;</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&rsquo;t tested this as thoroughly</del> I haven&rsquo;t run into this issue for nearly a month, so it seems to be fixed. But there&rsquo;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-&lt;index&gt;</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">&#39;</span>((<span style="color:#666">0</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;misc&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#666">1</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;core&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#666">2</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;browser&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#666">3</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;comms&#34;</span>) +</span></span><span style="display:flex;"><span> (<span style="color:#666">4</span> <span style="color:#666">.</span> <span style="color:#ba2121">&#34;dev&#34;</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&rsquo;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">&#34;Firefox&#34;</span> <span style="color:#ba2121">&#34;Nightly&#34;</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">&#34;browser&#34;</span>)) +</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">&#34;Alacritty&#34;</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">&#34;term&#34;</span>)) +</span></span><span style="display:flex;"><span> ((<span style="color:#008000">or</span> <span style="color:#ba2121">&#34;VK&#34;</span> <span style="color:#ba2121">&#34;Slack&#34;</span> <span style="color:#ba2121">&#34;Discord&#34;</span> <span style="color:#ba2121">&#34;TelegramDesktop&#34;</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">&#34;comms&#34;</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">&#39;exwm-manage-finish-hook</span> <span style="color:#00f">#&#39;</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&rsquo;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">&#39;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 &ldquo;s&rdquo; 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 &ldquo;s&rdquo; / <code>M-x pomm-start</code> or stopped competely with &ldquo;S&rdquo; / <code>M-x pomm-stop</code>.</li> +<li><strong>Running</strong>. Can be paused with &ldquo;p&rdquo; / <code>M-x pomm-pause</code> or stopped with &ldquo;S&rdquo; / <code>M-x pomm-stop</code>.</li> +</ul> +<p>The state of the timer can be reset with &ldquo;R&rdquo; or <code>M-x pomm-reset</code>.</p> +<p>&ldquo;u&rdquo; updates the transient buffer. The update is manual because I didn&rsquo;t figure out how to automate this, and I think this is not <em>really</em> necessary.</p> +<p>With &ldquo;r&rdquo; or <code>M-x pomm-set-context</code> you can set the current &ldquo;context&rdquo;, 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 &ldquo;s&rdquo; or <code>M-x pomm-third-time-start</code>.</li> +<li><strong>Running</strong>. Can be stopped with &ldquo;S&rdquo; or <code>M-x pomm-third-time-stop</code>. This resets the accumulated break time.</li> +</ul> +<p>Use &ldquo;b&rdquo; 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">&#39;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>&lt;executable&gt; /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">&#39;pomm-on-tick-hook</span> <span style="color:#19177c">&#39;pomm-update-mode-line-string</span>) +</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">&#39;pomm-on-status-changed-hook</span> <span style="color:#19177c">&#39;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 &gt;&gt; /dev/null; <span style="color:#008000;font-weight:bold">then</span> +</span></span><span style="display:flex;"><span> emacsclient --eval <span style="color:#ba2121">&#34;(if (boundp &#39;pomm-current-mode-line-string) pomm-current-mode-line-string \&#34;\&#34;) &#34;</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">&#34;%F %T&#34;</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&rsquo;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&rsquo;t mean something horrible in some language I don&rsquo;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 &ldquo;Generate Access Token&rdquo; 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">&#34;My_Online/APIs/genius.com&#34;</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&rsquo;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&rsquo;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&rsquo;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&rsquo; 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 &ldquo;cover_small.jpg&rdquo;, &ldquo;cover_med.jpg&rdquo;, &ldquo;cover_large.jpg&rdquo;.</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">&#39;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&rsquo;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.&lt;extension&gt;</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&rsquo;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.

+
+
+ +

The available backends are genius.com and music.163.com.

+

Installation

+

The package is available on MELPA. Install it however you normally install packages, I prefer use-package with straight:

+
(use-package lyrics-fetcher
+  :straight t
+  :after (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:

+
(setq lyrics-fetcher-genius-access-token
+      (password-store-get "My_Online/APIs/genius.com"))
+

But of course, you can just hardcode the string.

+

Usage

+

Available commands:

+
    +
  • +

    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.

+

The function has to receive 3 arguments:

+
    +
  • track - a string or alist, as outlined above.
  • +
  • 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.

+ +
+ +
+ +
+ + diff --git a/packages/micromamba/index.html b/packages/micromamba/index.html new file mode 100644 index 0000000..afd59e3 --- /dev/null +++ b/packages/micromamba/index.html @@ -0,0 +1,153 @@ + + + + + + micromamba.el + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ micromamba.el + + + +

+
+

+ micromamba.el + + + +

+
+
+ +

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-package micromamba
+  :straight t)
+

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.

+ +
+ +
+ +
+ + diff --git a/packages/org-clock-agg/index.html b/packages/org-clock-agg/index.html new file mode 100644 index 0000000..a5dc5ba --- /dev/null +++ b/packages/org-clock-agg/index.html @@ -0,0 +1,367 @@ + + + + + + org-clock-agg + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ org-clock-agg + + + +

+
+

+ org-clock-agg + + + +

+

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:

+
(use-package org-clock-agg
+  :straight (:host github :repo "SqrtMinusOne/org-clock-agg"))
+

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:

+
(setq org-clock-agg-files-preset
+      `(("Org Agenda + Archive"
+	 .
+	 ,(append (org-agenda-files)
+		  (cl-remove-if
+		   (lambda (f) (string-match-p (rx "." eos) f))
+		   (directory-files (concat org-directory "/archive/") t))))))
+

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:

+
* Tasks
+** DONE Thing 1
+:LOGBOOK:
+CLOCK: [2023-12-13 Wed 19:01]--[2023-12-13 Wed 19:29] =>  0:28
+CLOCK: [2023-12-13 Wed 19:30]--[2023-12-13 Wed 19:40] =>  0:10
+:END:
+

And predicates “Org file”, “Day”, and “Outline path”, the records for “Thing 1” will be processed as follows:

+
    +
  • “Day” -> 2023-12-13
  • +
  • “Org file” -> tasks.org
  • +
  • “Outline path” -> Tasks, Thing 1
  • +
+

Consequently, the node will be placed at the path 2023-12-13 / tasks.org / Tasks / Thing 1 in the resulting tree:

+
* Results                                                  Root    0:38
+** 2023-12-13                                               Day    0:38
+*** tasks.org                                          Org File    0:38
+**** Tasks                                         Outline path    0:38
+***** Thing 1                                      Outline path    0:38
+- [2023-12-13 Wed 19:01]--[2023-12-13 Wed 19:29] =>  0:28 : DONE Thing 1
+- [2023-12-13 Wed 19:30]--[2023-12-13 Wed 19:40] =>  0:10 : DONE Thing 1
+

The following built-in predicates are currently available:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameCommentCustomization variables
Category
Org file
Outline path
TagsSorted alphabetically
HeadlineLast item of the outline path
Dayorg-clock-agg-day-format
Weekorg-clock-agg-week-format
Monthorg-clock-agg-month-format
TODO keyword
Is done
Selected propsorg-clock-agg-properties
+

Ensure to use setopt to set the variables; otherwise, the customization logic will not be invoked:

+
(setopt org-clock-agg-properties '("PROJECT_NAME"))
+

Refer also to custom grouping predicates.

+

Customization

+

Node Formatting

+

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:

+
(org-clock-agg-defgroupby <name>
+  :key1 value1
+  :key2 value2
+  <body>)
+

The available keyword arguments include:

+
    +
  • :readable-name: Function name for the UI.
  • +
  • :default-sort: Default sorting function.
  • +
+

The body binds two variables - elem and extra-params, and must return a list of strings.

+

The elem variable is an alist that represents one org-clock record. The keys are as follows:

+
    +
  • :start: Start time in seconds since the epoch
  • +
  • :end: End time in seconds since the epoch
  • +
  • :duration: Duration in seconds
  • +
  • :headline: Instance of org-element for the headline
  • +
  • :tags: List of tags
  • +
  • :file: File name
  • +
  • :outline-path: titles of all headlines from the root to the current headline
  • +
  • :properties: List of properties; org-clock-agg-properties sets the selection list
  • +
  • :category: Category of the current headline.
  • +
+

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.

+

For instance:

+
(setq org-clock-agg-extra-params
+      '(("Events: Offline / Online" . (checkbox :extras-key :events-online))))
+

This adds a checkbox to the form that appears as:

+
Events: Offline / Online [ ]
+

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.

+

This can be done the following way:

+
(org-clock-agg-defgroupby event
+  :readable-name "Event"
+  :default-sort total
+  (let* ((title (org-element-property :raw-value (alist-get :headline elem)))
+	 (is-meeting (or (string-match-p "meeting" (downcase title))
+			 (seq-contains-p (alist-get :tags elem) "mt")))
+	 (is-offline (or (string-match-p "offline" (downcase title))
+			 (seq-contains-p (alist-get :tags elem) "offline")))
+	 (title-without-stuff (string-trim
+			       (replace-regexp-in-string
+				(rx (or
+				     (group (+ (or digit ".")))
+				     "(offline)"
+				     (seq "[" (+ alnum) "]") ))
+				"" title))))
+    (when is-meeting
+      `("Meeting"
+	,@(when (alist-get :events-online extra-params)
+	    (if is-offline '("Offline") '("Online")))
+	,title-without-stuff))))
+

For the following result:

+
* Results
+** Meetings
+*** Some meeting
+*** Another meeting
+** Ungrouped
+

This can be coupled with a project predicate to analyze the time spent per project in a particular kind of meeting.

+ +
+ +
+ +
+ + diff --git a/packages/org-journal-tags/index.html b/packages/org-journal-tags/index.html new file mode 100644 index 0000000..7d49655 --- /dev/null +++ b/packages/org-journal-tags/index.html @@ -0,0 +1,309 @@ + + + + + + org-journal-tags + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ org-journal-tags + + + +

+
+

+ org-journal-tags + + + +

+
+
+ +

A package to make sense of my life org-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:

+
(use-package org-journal-tags
+  :straight t
+  :after (org-journal)
+  :config
+  (org-journal-tags-autosync-mode))
+

Basic usage

+

Adding tags

+

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:

+
[[org-journal:<tag-name>::<number-of-paragraphs>][<tag-description>]]
+

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:

+
[[org-journal:<kind>:<tag-name>][<tag-description>]]
+[[org-journal:<kind>:<tag-name>::<number-of-paragraphs>][<tag-description>]]
+

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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandSet operationDescription
Unionold ∪ newAdd records of the new query to the displayed records
Intersectionold ∩ newLeave only those records that are both displayed and in the new query
Difference from currentold \ newExclude records of the new query from the displayed records
Difference to currentnew \ oldExclude 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:

+
(defun my/set-journal-header ()
+  (org-journal-tags-prop-apply-delta :add (list (format "host.%s" (system-name))))
+  (when (boundp 'my/loc-tag)
+    (org-journal-tags-prop-apply-delta :add (list my/loc-tag))))
+
+(add-hook 'org-journal-after-entry-create-hook
+	  #'my/set-journal-header)
+

Encryption

+

There are two ways how org-journal can be encrypted:

+
    +
  • With org-crypt, by setting org-journal-enable-encryption.
  • +
  • With epa, by setting org-journal-encrypt-journal.
  • +
+

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:

+
(run-with-idle-timer (* 60 15) t #'org-journal-tags-cache-reset)
+

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:

+
(setq org-journal-tags-db-file (concat user-emacs-directory "var/org-journal-tags/index.gpg"))
+

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:

+
(run-with-idle-timer (* 60 15) t #'org-journal-tags-db-unload)
+

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-union-refs - union
  • +
  • org-journal-tags--query-diff-refs - difference
  • +
  • org-journal-tags--query-intersect-refs - intersection
  • +
  • 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.

+ +
+ +
+ +
+ + diff --git a/packages/password-store-ivy/index.html b/packages/password-store-ivy/index.html new file mode 100644 index 0000000..c15ec18 --- /dev/null +++ b/packages/password-store-ivy/index.html @@ -0,0 +1,183 @@ + + + + + + password-store-ivy + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ password-store-ivy + + + +

+
+

+ password-store-ivy + + + +

+

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:

+
(use-package password-store-ivy
+  :straight (:host github :repo "SqrtMinusOne/password-store-ivy")
+  :after (exwm))
+

This package types stuff with xdotool, so you need to have that available in your $PATH.

+

Usage

+

Emacs’ built-in password store integration has to be set up.

+

The only command is M-x password-store-ivy, which invokes Ivy to select an entry from the pass database. Available commands in the selection buffer:

+
    +
  • M-a. Perform autotype
  • +
  • M-p. Type password
  • +
  • M-u. Type username
  • +
  • M-U. Type url
  • +
  • M-f. Select a field to type
  • +
+

Customization

+

There are a few parameters that control delays:

+
    +
  • password-store-ivy-initial-wait controls the initial delay before starting to type a sequence (in milliseconds)
  • +
  • password-store-ivy-delay controls the delay between typing characters (in milliseconds)
  • +
+

There is also password-store-ivy-sequences that determines the sequence of actions password-store-ivy performs.

+

It is an alist with the following required keys (corresponding to the basic actions):

+
    +
  • autotype
  • +
  • password
  • +
  • username
  • +
  • url
  • +
+

The values are lists of the following elements:

+
    +
  • wait. Wait for password-store-ivy-initial-wait milliseconds
  • +
  • (wait <milliseconds>). Wait for <milliseconds>.
  • +
  • (key <key>). Type <key>.
  • +
  • (field <field>). Type <field> of entry.
  • +
+

For example, the starting values:

+
'((autotype . (wait
+	       (field . "username")
+	       (key . "Tab")
+	       (field . secret)
+	       (key . "Return")))
+  (password . (wait (field . secret)))
+  (username . (wait (field . "username")))
+  (url . (wait (field . "url"))))
+

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:

+
<pass>
+username: thexcloud@gmail.com
+url: <url>
+sequence-autotype: (wait (field . "username") (key . "Tab") (key . "Tab") (field . secret) (key . "Return"))
+
+
+ +
+ +
+ + diff --git a/packages/perspective-exwm/index.html b/packages/perspective-exwm/index.html new file mode 100644 index 0000000..859ada7 --- /dev/null +++ b/packages/perspective-exwm/index.html @@ -0,0 +1,216 @@ + + + + + + perspective-exwm + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ perspective-exwm + + + +

+
+

+ perspective-exwm + + + +

+
+
+ +

A couple of tricks and fixes to make using EXWM and perspective.el a better experience.

+

Installation

+

This package is available on MELPA. Install it however you usually install packages, I use use-package and straight.el:

+
(use-package perspective-exwm
+  :straight t)
+

Or clone the repository, add the package to the load-path and load it with require.

+

The package provides a minor mode, perspective-exwm-mode, which is meant to be loaded before exwm-init. For instance, if you use use-package:

+
(use-package exwm
+  :config
+  ...
+  (perspective-exwm-mode)
+  (exwm-init))
+

Usage and details

+
    +
  • +

    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:

    +
    (setq perspective-exwm-override-initial-name
    +    '((0 . "misc")
    +      (1 . "core")
    +      (2 . "browser")
    +      (3 . "comms")
    +      (4 . "dev")))
    +

    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:

    +
    (defun my/exwm-configure-window ()
    +  (interactive)
    +  (pcase exwm-class-name
    +    ((or "Firefox" "Nightly")
    +     (perspective-exwm-assign-window
    +      :workspace-index 2
    +      :persp-name "browser"))
    +    ("Alacritty"
    +     (perspective-exwm-assign-window
    +      :persp-name "term"))
    +    ((or "VK" "Slack" "Discord" "TelegramDesktop")
    +     (perspective-exwm-assign-window
    +      :workspace-index 3
    +      :persp-name "comms"))))
    +
    +(add-hook 'exwm-manage-finish-hook #'my/exwm-configure-window)
    +
  • +
+

Known issues

+
    +
  • perspective-exwm-move-to-workspace kills X windows in the perspective it tries to move. Have no idea how to fix this at the moment.
  • +
+ +
+ +
+ +
+ + diff --git a/packages/pomm/index.html b/packages/pomm/index.html new file mode 100644 index 0000000..249f13d --- /dev/null +++ b/packages/pomm/index.html @@ -0,0 +1,320 @@ + + + + + + pomm.el + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ pomm.el + + + +

+
+

+ pomm.el + + + +

+
+
+ +

Implementation of Pomodoro and Third Time techniques for Emacs.

+
+
+ +

Features:

+
    +
  • 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.

+
M-x package-install pomm
+

My preferred way is use-package with straight.el:

+
(use-package pomm
+  :straight t
+  :commands (pomm pomm-third-time))
+

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-group pomm 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:

+
(setq alert-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:

+
(add-hook 'pomm-on-tick-hook 'pomm-update-mode-line-string)
+(add-hook 'pomm-on-status-changed-hook 'pomm-update-mode-line-string)
+

Create a script like this:

+
if ps -e | grep emacs >> /dev/null; then
+    emacsclient --eval "(if (boundp 'pomm-current-mode-line-string) pomm-current-mode-line-string \"\") " | xargs echo -e
+fi
+

And add a polybar module definition to your polybar config:

+
[module/pomm]
+type = custom/script
+exec = /home/pavel/bin/polybar/pomm.sh
+interval = 1
+

State file location

+

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:

+
(setq pomm-csv-history-file-timestamp-format "%F %T")
+

The format is the same as in format-time-string.

+

Alternatives

+

There is a number of packages with a similar purpose, here is a rough comparison of features:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Package3rd party integrationsControl method (1)Persistent historyPersistent stateNotifications
pomm.el-transient.elCSV+alert.el + sounds
org-pomodoroOrg Mode!via Org commandsvia Org mode-alert.el + sounds
pomidor-self-cooked interactive buffercustom delimited format?+, but saving on-demandalert.el + sounds
pomodoro.el----notifications.el + sounds
tomatinho-self-cooked interactive buffer--message + sounds
redtick-mode-line icon+-sounds
gtk-pomodoro-indicatorGTK panelCLI--, but the program is independent from EmacsGTK notifications
+

Be sure to check those out if this one doesn’t quite fit your workflow!

+

(1) Means of timer control with exception of Emacs interactive commands

+

Also take a look at org-pomodoro-third-time, which adapts org-pomodoro for the Third Time technique.

+

P.S.

+

The package name is not an abbreviation. I just hope it doesn’t mean something horrible in some language I don’t know.

+

The sounds are made by Mike Koening under CC BY 3.0.

+ +
+ +
+ +
+ + diff --git a/packages/reverso/index.html b/packages/reverso/index.html new file mode 100644 index 0000000..dbb2cad --- /dev/null +++ b/packages/reverso/index.html @@ -0,0 +1,271 @@ + + + + + + reverso.el + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+

+ reverso.el + + + +

+
+

+ reverso.el + + + +

+

Emacs client for Reverso. The implemented features are:

+ +

Installation

+

The package isn’t yet available anywhere but in this repository. My preferred way for such cases is use-package and straight.el:

+
(use-package reverso
+  :straight (:host github :repo "SqrtMinusOne/reverso.el"))
+

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:

+
(setq reverso-languages '(english german russian))
+

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.

+

Alternatives and Observations

+

A widely recognized translation service is Google Translate, so of course, there’s an Emacs client for it.

+

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.

+ +
+ +
+ +
+ + diff --git a/perspective-exwm-img/cycle-buffers.png b/perspective-exwm-img/cycle-buffers.png new file mode 100644 index 0000000..d9343ba Binary files /dev/null and b/perspective-exwm-img/cycle-buffers.png differ diff --git a/perspective-exwm-img/switch-perspective.png b/perspective-exwm-img/switch-perspective.png new file mode 100644 index 0000000..1748302 Binary files /dev/null and b/perspective-exwm-img/switch-perspective.png differ diff --git a/pomm-img/screenshot-tt.png b/pomm-img/screenshot-tt.png new file mode 100644 index 0000000..443badd Binary files /dev/null and b/pomm-img/screenshot-tt.png differ diff --git a/pomm-img/screenshot.png b/pomm-img/screenshot.png new file mode 100644 index 0000000..7caee4c Binary files /dev/null and b/pomm-img/screenshot.png differ diff --git a/reverso-img/grammar-buffer-res.png b/reverso-img/grammar-buffer-res.png new file mode 100644 index 0000000..50afb3f Binary files /dev/null and b/reverso-img/grammar-buffer-res.png differ diff --git a/reverso-img/grammar-buffer-transient.png b/reverso-img/grammar-buffer-transient.png new file mode 100644 index 0000000..c70e6e7 Binary files /dev/null and b/reverso-img/grammar-buffer-transient.png differ diff --git a/reverso-img/grammar-res.png b/reverso-img/grammar-res.png new file mode 100644 index 0000000..ee82eda Binary files /dev/null and b/reverso-img/grammar-res.png differ diff --git a/reverso-img/grammar-transient.png b/reverso-img/grammar-transient.png new file mode 100644 index 0000000..30df85a Binary files /dev/null and b/reverso-img/grammar-transient.png differ diff --git a/reverso-img/synonyms-res.png b/reverso-img/synonyms-res.png new file mode 100644 index 0000000..4e243a8 Binary files /dev/null and b/reverso-img/synonyms-res.png differ diff --git a/reverso-img/synonyms-transient.png b/reverso-img/synonyms-transient.png new file mode 100644 index 0000000..bc2f9e3 Binary files /dev/null and b/reverso-img/synonyms-transient.png differ diff --git a/reverso-img/translation-res.png b/reverso-img/translation-res.png new file mode 100644 index 0000000..ec290ce Binary files /dev/null and b/reverso-img/translation-res.png differ diff --git a/reverso-img/translation-transient.png b/reverso-img/translation-transient.png new file mode 100644 index 0000000..1186de2 Binary files /dev/null and b/reverso-img/translation-transient.png differ diff --git a/sitemap.xml b/sitemap.xml index 5044930..654e04e 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,14 +2,20 @@ + https://sqrtminusone.xyz/ + 2023-12-17T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/org-clock-agg/ + 2023-12-17T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/ + 2023-12-17T00:00:00+00:00 + https://sqrtminusone.xyz/posts/2023-11-11-index/ 2023-11-11T00:00:00+00:00 https://sqrtminusone.xyz/tags/emacs/ 2023-11-11T00:00:00+00:00 - - https://sqrtminusone.xyz/ - 2023-11-11T00:00:00+00:00 https://sqrtminusone.xyz/tags/orgmode/ 2023-11-11T00:00:00+00:00 @@ -19,6 +25,12 @@ https://sqrtminusone.xyz/tags/ 2023-11-11T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/biome/ + 2023-07-22T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/micromamba/ + 2023-06-20T00:00:00+00:00 https://sqrtminusone.xyz/posts/2023-04-13-emacs/ 2023-04-13T00:00:00+00:00 @@ -31,21 +43,48 @@ https://sqrtminusone.xyz/posts/2022-09-16-vosk/ 2022-09-16T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/reverso/ + 2022-08-28T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/elfeed-sync/ + 2022-05-29T00:00:00+00:00 https://sqrtminusone.xyz/posts/2022-05-09-pdf/ 2022-05-10T00:00:00+00:00 https://sqrtminusone.xyz/tags/org-mode/ 2022-05-10T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/avy-dired/ + 2022-04-01T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/elfeed-summary/ + 2022-03-26T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/password-store-ivy/ + 2022-02-13T00:00:00+00:00 https://sqrtminusone.xyz/posts/2022-02-12-literate/ 2022-02-12T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/org-journal-tags/ + 2022-02-06T00:00:00+00:00 https://sqrtminusone.xyz/tags/exwm/ 2022-01-03T00:00:00+00:00 https://sqrtminusone.xyz/posts/2022-01-03-exwm/ 2022-01-03T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/exwm-modeline/ + 2021-12-22T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/perspective-exwm/ + 2021-12-01T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/pomm/ + 2021-11-05T00:00:00+00:00 https://sqrtminusone.xyz/posts/2021-10-04-emacs-i3/ 2021-10-06T00:00:00+00:00 @@ -58,6 +97,9 @@ https://sqrtminusone.xyz/posts/2021-09-07-emms/ 2021-09-08T00:00:00+00:00 + + https://sqrtminusone.xyz/packages/lyrics-fetcher/ + 2021-08-14T00:00:00+00:00 https://sqrtminusone.xyz/tags/org/ 2021-05-01T00:00:00+00:00 @@ -75,6 +117,20 @@ 2021-02-01T00:00:00+00:00 https://sqrtminusone.xyz/categories/ + + https://sqrtminusone.xyz/configs/ + + https://sqrtminusone.xyz/configs/console/ + + https://sqrtminusone.xyz/configs/desktop/ + + https://sqrtminusone.xyz/configs/emacs/ + + https://sqrtminusone.xyz/configs/guix/ + + https://sqrtminusone.xyz/configs/mail/ + + https://sqrtminusone.xyz/configs/readme/ https://sqrtminusone.xyz/emacs-packages/