Compare commits

..

No commits in common. "master" and "5.0.0" have entirely different histories.

222 changed files with 11342 additions and 23487 deletions

1
.gitignore vendored
View file

@ -29,4 +29,3 @@
user-emacs.*
prot-emacs-pre-custom.*
prot-emacs-post-custom.*
*-autoloads.el

View file

@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Dotfile collection with post-install customisations for GNU/Linux
Copyright (C) 2019-2026 Protesilaos
Copyright (C) 2019 Protesilaos Stavrou
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Protesilaos's Dotfiles Copyright (C) 2019-2026 Protesilaos
Protesilaos Stavrou's Dotfiles Copyright (C) 2019 Protesilaos Stavrou
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

View file

@ -1,5 +1,5 @@
[user]
name = Protesilaos
name = Protesilaos Stavrou
email = info@protesilaos.com
signingkey = C3C6BC9BBE2F62C7C586EBAA99BD6459CD5CA3EA
[core]

View file

@ -1,4 +1,4 @@
# My Emacs and other configurations for Linux
# My Emacs and other configurations for Arch Linux
This is the set of files that powers my day-to-day computing experience.
GNU Emacs is its centrepiece, with other programs providing ancillary
@ -26,10 +26,12 @@ or contribute any fixes, if you will.
## Emacs setup
I do not recommend you reproduce my Emacs setup because I do not
design it as a distribution for other people. If you insist though,
the files are in the `emacs` directory. Add them to your home
directory with:
I do not recommend you reproduce my Emacs setup because I do not use
the de facto standard of `use-package` to configure packages. I
prefer a simpler approach.
If you insist though, the files are in the `emacs` directory. Add them
to your home directory with:
```sh
/path/to/prot-dotfiles $ stow -t "$HOME" emacs
@ -56,11 +58,11 @@ run stow again with the `-R` flag:
## Window managers
My tiling window managers for Xorg are bspwm, herbstluftwm, and i3 (in
no particular order). They are all configured to be almost the same:
My tiling window managers are bspwm, herbstluftwm, and i3 (in no
particular order). They are all configured to be almost the same:
they share settings for the wallpaper, system panel, theme, keyboard
layout, and display compositor. All settings are in the `xorg-twm`
directory ("twm" stands for "tiling window manager"). What
layout, and display compositor. All settings are in the `xorg-twm`
directory ("twm" stands for "tiling window manager"). What
differentiates the window managers is their individual features.
+ **bspwm:** I have been using it for years and consider it top-notch.
@ -70,26 +72,25 @@ differentiates the window managers is their individual features.
them.
+ **herbstluftwm (hlwm):** herbstluftwm prioritises manual tiling
methods. It has the concept of "frames", which are rectangles that
include regular app windows. Each frame has its own dimensions are
layout. For example, it is possible to have a frame where windows
are vertically layed out one below the other and another frame with
a tabbed layout. What makes this even more appealing, is the ease
with which a workspace can be preconfigured (or dymanically
adjusted) to a given arrangement of frames and their layouts. I
think herbstluftwm is optimal for screens wider than the the regular
1920x1080. Otherwise, the manual tiling method adds cognitive load
that the user does not have while using bspwm.
methods and can treat arbitrary rectangles of a monitor as virtual
monitors. The virtual monitors feature is perfect for anyone with a
widescreen display. The one I have (which is not mine, but anyway)
is 2560x1080, so I split it by default into a regular 1920x1080 area
and another "sidebar" of 640x1080. herbstluftwm has the concept of
"frame", which is a container of regular windows. Each frame can
have its own layout, including a vertical/horizontal stack, grid,
and tabbed.
+ **i3 (or i3wm):** The first tiling window manager I ever used (circa
2017). The reason I abandoned it back in the day in favour of bspwm
2017). The reason I abandoned it back in the day in favour of bspwm
is because its default tiling method requires manual intervention to
change the split direction. On a small laptop monitor, I prefer this
to be done automatically, hence bspwm. Though on a wide monitor, I
typically keep the split direction constant. i3 has the concept of
the "container" (same as the herbstluftwm "frame"), which can be set
to stacked, tabbed, or tiled layouts. As such, i3 is somewhere
between bspwm and herbstluftwm. Choose according to your needs.
change the split direction. On a small laptop monitor, I prefer
this to be done automatically, hence bspwm. Though on a wide
monitor, I typically keep the split direction constant. i3 has the
concept of the "container" (same as herbstluftwm "frame"), which can
be set to stacked, tabbed, or tiled layouts. As such, i3 is
somewhere between bspwm and herbstluftwm. Choose according to your
needs.
Check the `xtwm-key-binding-cheatsheet.md` file for an overview of
their key bindings.
@ -99,13 +100,9 @@ always use herbstluftwm because of its ability to support virtual
monitors. Otherwise I oscillate between bspwm and i3, depending on
the machine I am using.
What about Wayland? I have experimented with it on GNOME where it
works fine. I even have some configurations for tiling compositors,
but am not happy with those. The Wayland ecosystem is not ready yet
for my use-case: I am missing something like sxhkd and certain
applications do not work properly on it, such screen sharing. I will
check again some time over the medium term, as Wayland seems
inevitable.
What about Wayland? It is not ready yet: I am missing something like
sxhkd and certain applications do not work properly on it. I may
check again in a few years.
## Copying

View file

@ -4,7 +4,7 @@
# Implements the formula provided by the Web Content Accessibility
# Guidelines (WCAG) 2.0: https://www.w3.org/TR/WCAG20-TECHS/G18.html
#
# CLR is part of my dotfiles: https://github.com/protesilaos/dotfiles.
# CLR is part of my dotfiles: https://git.sr.ht/~protesilaos/dotfiles.
# Apart from its utility, this script has been developed as a means of
# practicing certain shell scripting techniques.
#
@ -12,7 +12,7 @@
# guidelines), refer to my Tempus themes (partially incorporated in my
# dotfiles as well): https://gitlab.com/protesilaos/tempus-themes.
#
# Copyright (C) 2019-2026 Protesilaos <info@protesilaos.com>
# Copyright (c) 2019 Protesilaos Stavrou <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by

View file

@ -2,7 +2,7 @@
# Delight (DE light) --- Toggle DE/WM and Emacs themes
#
# Copyright (C) 2020-2026 Protesilaos <info@protesilaos.com>
# Copyright (c) 2020-2023 Protesilaos Stavrou <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -23,19 +23,12 @@
# centred around my BSPWM/HerbstluftWM or Sway setup. For the standard
# DEs, this has been tested with GNOME 42 and Xfce 4.16.
#
# Part of my dotfiles: <https://github.com/protesilaos/dotfiles>.
# Part of my dotfiles: <https://git.sr.ht/~protesilaos/dotfiles>.
### Code:
#### General
gtk_theme_light="Adwaita"
gtk_theme_dark="Adwaita-dark"
# icon_theme_light="Papirus-Light"
# icon_theme_dark="Papirus-Dark"
icon_theme_light="Adwaita"
icon_theme_dark="Adwaita"
_depcheck ()
{
command -v "$1" > /dev/null || { echo "Missing dependency: $1."; return 1; }
@ -57,14 +50,20 @@ _emacs ()
pgrep -x emacs > /dev/null || return 1
local light_theme
local dark_theme
light_theme="modus-operandi"
dark_theme="modus-vivendi"
# Just switch to the appropriate theme for Emacs:
# https://github.com/protesilaos/modus-themes
# https://git.sr.ht/~protesilaos/modus-themes
case "$1"
in
# TODO 2023-03-01: Define a function to load appropriate Emacs
# theme.
l*) emacsclient -e "(when (and (functionp 'modus-themes-get-themes) (modus-themes-get-themes)) (modus-themes-load-random-light))" > /dev/null ;;
d*) emacsclient -e "(when (and (functionp 'modus-themes-get-themes) (modus-themes-get-themes)) (modus-themes-load-random-dark))" > /dev/null ;;
l*) emacsclient -e "(when (and (functionp 'modus-themes--current-theme) (modus-themes--current-theme)) (load-theme '${light_theme} :no-confirm))" > /dev/null ;;
d*) emacsclient -e "(when (and (functionp 'modus-themes--current-theme) (modus-themes--current-theme)) (load-theme '${dark_theme} :no-confirm))" > /dev/null ;;
esac
}
@ -78,7 +77,7 @@ _xterm ()
[ -f "$xresources" ] || { echo "No Xresources file"; exit 1; }
[ -f "$active_theme" ] || { echo "No active Xresources theme"; exit 1; }
case $wm_theme
case ${1:-$wm_theme}
in
light) _sed "s,vivendi,operandi," "$active_theme" ;;
dark) _sed "s,operandi,vivendi," "$active_theme" ;;
@ -88,30 +87,7 @@ _xterm ()
# Another one of my scripts for live recolouring running terminal
# emulators, by using escape sequences.
repaint_terminals.sh &
}
_xfce4terminal ()
{
_depcheck xfce4-terminal
active_theme="$HOME"/.config/xfce4/terminal/terminalrc
palette_light="#000000;#a60000;#006800;#6f5500;#0031a9;#8f0075;#005e8b;#aaaaaa;#505050;#972500;#00663f;#884900;#0000b0;#531ab6;#005f5f;#ffffff;"
palette_dark="#000000;#ff5f59;#44bc44;#d0bc00;#2fafff;#f78fe7;#00d3d0;#aaaaaa;#505050;#ff6b55;#00c06f;#fec43f;#00bcff;#b6a0ff;#6ae4b9;#ffffff;"
case "$1"
in
light)
_sed "s,ColorForeground=.*,ColorForeground=#000000," "$active_theme"
_sed "s,ColorBackground=.*,ColorBackground=#ffffff," "$active_theme"
_sed "s,ColorPalette=.*,ColorPalette=$palette_light," "$active_theme"
;;
dark)
_sed "s,ColorForeground=.*,ColorForeground=#ffffff," "$active_theme"
_sed "s,ColorBackground=.*,ColorBackground=#000000," "$active_theme"
_sed "s,ColorPalette=.*,ColorPalette=$palette_dark," "$active_theme"
;;
esac
repaint_terminals &
}
# TODO 2021-08-27: dunst
@ -187,8 +163,8 @@ _xsettingsd ()
if [ -f "$xsettingsd" ]
then
_sed "s,\(.*\/ThemeName\) \"[0-9a-zA-Z-]*\",\1 \"${1}\"," "$xsettingsd"
_sed "s,\(.*\/IconThemeName\) \"[0-9a-zA-Z-]*\",\1 \"${2}\"," "$xsettingsd"
_sed "s,\(.*\/ThemeName\) \"[a-zA-Z-]*\",\1 \"${1}\"," "$xsettingsd"
_sed "s,\(.*\/IconThemeName\) \"[a-zA-Z-]*\",\1 \"${2}\"," "$xsettingsd"
fi
# This is a very lightweight program that simply loads the
@ -201,12 +177,12 @@ _xsettingsd ()
case "$1"
in
light)
_gtk_common "$gtk_theme_light" "$icon_theme_light"
_gtk_live "$gtk_theme_light" "$icon_theme_light"
_gtk_common 'Adwaita' 'Adwaita'
_gtk_live 'Adwaita' 'Adwaita'
;;
dark)
_gtk_common "$gtk_theme_dark" "$icon_theme_dark"
_gtk_live "$gtk_theme_dark" "$icon_theme_dark"
_gtk_common 'Adwaita-dark' 'Adwaita'
_gtk_live 'Adwaita-dark' 'Adwaita'
;;
esac
}
@ -227,7 +203,6 @@ _bspwm_or_hlwm ()
_emacs "$style" &
_xsettingsd "$style" &
_xterm &
_xfce4terminal "$style" &
# # NOTE 2021-09-25: I tried alacritty for a while, but the way it
# reads the fonts on Xorg makes the point size larger than expected.
# Also, it seems to miss some smaller sizes, so I cannot use it
@ -237,7 +212,7 @@ _bspwm_or_hlwm ()
_feh &
_rofi &
if [ -f "$focus_mode_status" ] && [ "$(sed 1q "$focus_mode_status")" = off ]
if [ -f $focus_mode_status ] && [ "$(sed 1q "$focus_mode_status")" = off ]
then
_polybar launch
else
@ -296,16 +271,21 @@ _i3 ()
#### Gsettings
# Get or set the GTK theme
_gsettings_gtk_act_theme_interface ()
_gsettings ()
{
gsettings "$1" org.gnome.desktop.interface gtk-theme "${@:2}"
gsettings "$1" "$2" "${@:3}"
}
# Get or set the GTK theme
_gsettings_gtk_act ()
{
_gsettings "$1" "$2" gtk-theme "${@:3}"
}
# Set the Color Scheme
_gsettings_color_scheme_set ()
{
gsettings set org.gnome.desktop.interface color-scheme "$1"
_gsettings 'set' 'org.gnome.desktop.interface' 'color-scheme' "$1"
}
##### Gnome
@ -313,21 +293,17 @@ _gsettings_color_scheme_set ()
_gnome_theme ()
{
_depcheck gsettings
interface="org.gnome.desktop.interface"
if [ "$(_gsettings_gtk_act_theme_interface get)" = "'$gtk_theme_light'" ]
then
_gsettings_gtk_act_theme_interface set "$gtk_theme_dark"
else
_gsettings_gtk_act_theme_interface set "$gtk_theme_light"
fi
if [ "$(gsettings get org.gnome.desktop.interface color-scheme)" = "'prefer-light'" ]
if [ "$(_gsettings_gtk_act 'get' $interface)" = "'Adwaita'" ]
then
style=dark
_gsettings_color_scheme_set prefer-dark
_gsettings_gtk_act 'set' $interface 'Adwaita-dark'
_gsettings_color_scheme_set 'prefer-dark'
else
style=light
_gsettings_color_scheme_set prefer-light
_gsettings_gtk_act 'set' $interface 'Adwaita'
_gsettings_color_scheme_set 'prefer-light'
fi
}
@ -337,6 +313,31 @@ _gnome ()
_emacs "$style"
}
##### Mate
_mate_theme ()
{
_depcheck gsettings
interface="org.mate.interface"
if [ "$(_gsettings_gtk_act 'get' $interface)" = "'Adwaita'" ]
then
style=dark
_gsettings_gtk_act 'set' $interface 'Adwaita-dark'
_gsettings_color_scheme_set 'prefer-dark'
else
style=light
_gsettings_gtk_act 'set' $interface 'Adwaita'
_gsettings_color_scheme_set 'prefer-light'
fi
}
_mate ()
{
_mate_theme
_emacs "$style"
}
#### Xfce
_xfquery_get ()
@ -351,27 +352,20 @@ _xfquery_set ()
_xfce ()
{
_depcheck gsettings
if [ "$(_xfquery_get 'xsettings' '/Net/ThemeName')" = "$gtk_theme_light" ]
if [ "$(_xfquery_get 'xsettings' '/Net/ThemeName')" = 'Adwaita' ]
then
style=dark
# _xfquery_set 'xfwm4' '/general/theme' 'adwaita-dark'
_gsettings_gtk_act_theme_interface set "$gtk_theme_dark"
_xfquery_set 'xsettings' '/Net/ThemeName' "$gtk_theme_dark"
_xfquery_set 'xsettings' '/Net/IconThemeName' "$icon_theme_dark"
_gsettings_color_scheme_set 'prefer-dark'
# _xfquery_set 'xfwm4' '/general/theme' 'Adwaita-dark'
_xfquery_set 'xsettings' '/Net/ThemeName' 'Adwaita-dark'
else
style=light
# _xfquery_set 'xfwm4' '/general/theme' 'adwaita'
_gsettings_gtk_act_theme_interface set "$gtk_theme_light"
_xfquery_set 'xsettings' '/Net/ThemeName' "$gtk_theme_light"
_xfquery_set 'xsettings' '/Net/IconThemeName' "$icon_theme_light"
_gsettings_color_scheme_set 'prefer-light'
# _xfquery_set 'xfwm4' '/general/theme' 'Adwaita'
_xfquery_set 'xsettings' '/Net/ThemeName' 'Adwaita'
fi
_emacs "$style" &
_xfce4terminal "$style" &
_gnome_theme
_emacs "$style"
_xterm "$style"
}
#### Sessions
@ -380,18 +374,19 @@ case "$DESKTOP_SESSION"
in
bspwm)
wm_executable=bspwm
conf_colors=bspwm_conf_colors.sh
conf_colors=bspwm_conf_colors
_bspwm_or_hlwm
;;
herbstluftwm)
wm_executable=herbstluftwm
conf_colors=herbstluftwm_conf_colors.sh
conf_colors=herbstluftwm_conf_colors
_bspwm_or_hlwm
;;
i3) _i3 ;;
gnome) _gnome ;;
mate) _mate ;;
xfce) _xfce ;;
esac

View file

@ -2,7 +2,7 @@
# pcb --- Print the terminal's active colours.
#
# Copyright (C) 2019-2026 Protesilaos <info@protesilaos.com>
# Copyright (c) 2019-2023 Protesilaos Stavrou <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by

View file

@ -2,7 +2,7 @@
# poweroptionsmenu --- rofi or dmenu interface for session actions.
#
# Copyright (C) 2019-2026 Protesilaos <info@protesilaos.com>
# Copyright (c) 2019-2023 Protesilaos Stavrou <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -27,7 +27,7 @@ _pkill() {
}
_kill_processes() {
_pkill bspwm_external_rules.sh
_pkill bspwm_external_rules
_pkill sxhkd
_pkill polybar
_pkill feh
@ -37,7 +37,9 @@ _kill_processes() {
# In case something stops working, we can call this script
# (Ctrl+Alt+Delete on my sxhkdrc) and apply the modifications again.
_kbd_reset() {
setxkbmap -layout 'us,gr' -option '' -option 'grp:win_space_toggle' -option 'compose:menu'
setxkbmap -layout 'us,gr' -option '' -option 'ctrl:nocaps' \
-option 'altwin:menu_win' -option 'caps:none' \
-option 'compose:ins' -option 'grp:win_space_toggle'
}
_check_systemd() {
@ -85,7 +87,7 @@ _dynamic_menu() {
# use printf to output array items on a new line
rofi -dmenu -i -no-custom -p 'Power options'
elif _checkdep dmenu; then
dmenu -i -p 'Power options'
my_dmenu -p 'Power options'
else
echo "Missing rofi or dmenu dependency." && exit 1
fi

View file

@ -2,7 +2,7 @@
# tmr --- Bash script to run a simple timer
#
# Copyright (C) 2019-2026 Protesilaos <info@protesilaos.com>
# Copyright (c) 2019-2023 Protesilaos Stavrou <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -24,7 +24,7 @@
#
# Distributed as part of my dotfiles, since I use it daily, though it
# technically is not an integral part of my custom desktop session:
# https://github.com/protesilaos/dotfiles.
# https://git.sr.ht/~protesilaos/dotfiles.
#
# TMR dependencies:
# mpv libnotify (libnotify-bin) sound-theme-freedesktop

View file

@ -26,31 +26,24 @@
# constant width of 300
width = 300
# The height of a single notification, excluding the frame.
# dynamic height from 0 to 300
height = (0, 300)
# constant height of 300
# height = 300
# NOTE: Dunst from version 1.11 and older don't support dynamic height
# and the given value is treated as the maximum height
# The maximum height of a single notification, excluding the frame.
height = 300
# Position the notification in the bottom right corner
# Position the notification in the top right corner
origin = bottom-right
# Offset from the origin
# NOTE: Dunst from version 1.11 and older use this alternative notation
# offset = 10x50
offset = (10, 25)
offset = 10x50
# Scale factor. It is auto-detected if value is 0.
scale = 0
# Maximum number of notification (0 means no limit)
notification_limit = 20
notification_limit = 0
### Progress bar ###
# Turn on the progress bar. It appears when a progress hint is passed with
# Turn on the progess bar. It appears when a progress hint is passed with
# for example dunstify -h int:value:12
progress_bar = true
@ -67,19 +60,6 @@
# Set the maximum width for the progress bar
progress_bar_max_width = 300
# Corner radius for the progress bar. 0 disables rounded corners.
progress_bar_corner_radius = 0
# Define which corners to round when drawing the progress bar. If progress_bar_corner_radius
# is set to 0 this option will be ignored.
progress_bar_corners = all
# Corner radius for the icon image.
icon_corner_radius = 0
# Define which corners to round when drawing the icon image. If icon_corner_radius
# is set to 0 this option will be ignored.
icon_corners = all
# Show how many messages are currently hidden (because of
# notification_limit).
@ -93,8 +73,7 @@
# Draw a line of "separator_height" pixel height between two
# notifications.
# Set to 0 to disable.
# If gap_size is greater than 0, this setting will be ignored.
separator_height = 2
separator_height = 1
# Padding between text and separator.
padding = 8
@ -112,12 +91,6 @@
# Defines color of the frame around the notification window.
frame_color = "#aaaaaa"
# Size of gap to display between notifications - requires a compositor.
# If value is greater than 0, separator_height will be ignored and a border
# of size frame_width will be drawn around each notification instead.
# Click events on gaps do not currently propagate to applications below.
gap_size = 0
# Define a color for the separator.
# possible values are:
# * auto: dunst tries to find a color fitting to the background;
@ -126,12 +99,7 @@
# * anything else will be interpreted as a X color.
separator_color = frame
# Sort type.
# possible values are:
# * id: sort by id
# * urgency_ascending: sort by urgency (low then normal then critical)
# * urgency_descending: sort by urgency (critical then normal then low)
# * update: sort by update (most recent always at the top)
# Sort messages by urgency.
sort = yes
# Don't remove messages, if the user is idle (no mouse or keyboard input)
@ -139,11 +107,11 @@
# Set to 0 to disable.
# A client can set the 'transient' hint to bypass this. See the rules
# section for how to disable this if necessary
# idle_threshold = 120
idle_threshold = 120
### Text ###
font = Sans 8
font = Monospace 9.5
# The spacing between lines. If the height is smaller than the
# font height, it will get raised to the font height.
@ -157,7 +125,7 @@
# <u>underline</u>
#
# For a complete reference see
# <https://docs.gtk.org/Pango/pango_markup.html>.
# <https://developer.gnome.org/pango/stable/pango-Markup.html>.
#
# strip: This setting is provided for compatibility with some broken
# clients that send markup even though it's not enabled on the
@ -216,28 +184,19 @@
### Icons ###
# Recursive icon lookup. You can set a single theme, instead of having to
# define all lookup paths.
enable_recursive_icon_lookup = true
# Set icon theme (only used for recursive icon lookup)
# icon_theme = Adwaita
# You can also set multiple icon themes, with the leftmost one being used first.
icon_theme = "Adwaita, Papirus-Dark, Papirus-Light"
# Align icons left/right/top/off
# Align icons left/right/off
icon_position = left
# Scale small icons up to this size, set to 0 to disable. Helpful
# for e.g. small files or high-dpi screens. In case of conflict,
# max_icon_size takes precedence over this.
min_icon_size = 32
min_icon_size = 0
# Scale larger icons down to this size, set to 0 to disable
max_icon_size = 128
max_icon_size = 32
# Paths to default icons (only necessary when not using recursive icon lookup)
icon_path = /usr/share/icons/gnome/16x16/status/:/usr/share/icons/gnome/16x16/devices/
# Paths to default icons.
icon_path = /usr/share/icons/Adwaita/16x16/status/:/usr/share/icons/Adwaita/16x16/devices/
### History ###
@ -259,10 +218,10 @@
# Always run rule-defined scripts, even if the notification is suppressed
always_run_script = true
# Define the title of the windows spawned by dunst (X11 only)
# Define the title of the windows spawned by dunst
title = Dunst
# Define the class of the windows spawned by dunst (X11 only)
# Define the class of the windows spawned by dunst
class = Dunst
# Define the corner radius of the notification window
@ -272,13 +231,6 @@
# notification height to avoid clipping text and/or icons.
corner_radius = 0
# Define which corners to round when drawing the window. If the corner radius
# is set to 0 this option will be ignored.
#
# Comma-separated list of the corners. The accepted corner values are bottom-right,
# bottom-left, top-right, top-left, top, bottom, left, right or all.
corners = all
# Ignore the dbus closeNotification message.
# Useful to enforce the timeout set by dunst configuration. Without this
# parameter, an application may close the notification sent before the
@ -288,7 +240,7 @@
### Wayland ###
# These settings are Wayland-specific. They have no effect when using X11
# Uncomment this if you want to let notifications appear under fullscreen
# Uncomment this if you want to let notications appear under fullscreen
# applications (default: overlay)
# layer = top
@ -343,25 +295,23 @@
background = "#222222"
foreground = "#888888"
timeout = 4
# Icon for notifications with low urgency
default_icon = dialog-information
# Icon for notifications with low urgency, uncomment to enable
#icon = /path/to/icon
[urgency_normal]
background = "#333333"
foreground = "#ffffff"
timeout = 6
override_pause_level = 30
# Icon for notifications with normal urgency
default_icon = dialog-information
# Icon for notifications with normal urgency, uncomment to enable
#icon = /path/to/icon
[urgency_critical]
background = "#900000"
foreground = "#ffffff"
frame_color = "#ff0000"
timeout = 0
override_pause_level = 60
# Icon for notifications with critical urgency
default_icon = dialog-warning
# Icon for notifications with critical urgency, uncomment to enable
#icon = /path/to/icon
# Every section that isn't one of the above is interpreted as a rules to
# override settings for certain messages.
@ -386,18 +336,9 @@
# new_icon
# set_stack_tag
# set_transient
# set_category
# timeout
# urgency
# icon_position
# skip_display
# history_ignore
# action_name
# word_wrap
# ellipsize
# alignment
# hide_text
# override_pause_level
#
# Shell-like globbing will get expanded.
#
@ -405,10 +346,6 @@
# GLib based applications export their desktop-entry name. In comparison to the appname,
# the desktop-entry won't get localized.
#
# You can also allow a notification to appear even when paused. Notification will appear whenever notification's override_pause_level >= dunst's paused level.
# This can be used to set partial pause modes, where more urgent notifications get through, but less urgent stay paused. To do that, you can override the following in the rules:
# override_pause_level = X
# SCRIPTING
# You can specify a script that gets run when the rule matches by
# setting the "script" option.

View file

@ -2,7 +2,7 @@
# dunst_toggle_notify --- Notify on Dunst pause/activation
#
# Copyright (C) 2021-2026 Protesilaos <info@protesilaos.com>
# Copyright (c) 2021-2023 Protesilaos Stavrou <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -19,7 +19,7 @@
#
### Commentary:
#
# Part of my dotfiles: <https://github.com/protesilaos/dotfiles>.
# Part of my dotfiles: <https://git.sr.ht/~protesilaos/dotfiles>.
command -v dunstctl > /dev/null || { echo "ERROR: No 'dunstctl'." ; exit 1 ; }

View file

@ -1,5 +1,32 @@
(defvar prot-emacs-tiling-window-manager-regexp
(regexp-opt '("bspwm" "herbstluftwm" "i3"))
"Regular expression to tiling window managers.
See definition of `prot-emacs-with-desktop-session'.")
(defmacro prot-emacs-with-desktop-session (&rest body)
"Expand BODY if desktop session is not a tiling window manager.
See `prot-emacs-tiling-window-manager-regexp' for what
constitutes a matching tiling window manager."
(declare (indent 0))
`(when-let ((session (getenv "DESKTOP_SESSION"))
((not (string-match-p session prot-emacs-tiling-window-manager-regexp))))
,@body))
(defun prot-emacs-add-to-list (list element)
"Add to symbol of LIST the given ELEMENT.
Simplified version of `add-to-list'."
(set list (cons element (symbol-value list))))
(prot-emacs-with-desktop-session
(mapc
(lambda (var)
(prot-emacs-add-to-list var '(width . (text-pixels . 900)))
(prot-emacs-add-to-list var '(height . (text-pixels . 600)))
(prot-emacs-add-to-list var '(scroll-bar-width . 10)))
'(default-frame-alist initial-frame-alist)))
(setq frame-resize-pixelwise t
frame-inhibit-implied-resize 'force
frame-inhibit-implied-resize t
frame-title-format '("%b")
ring-bell-function 'ignore
use-dialog-box t ; only for mouse events, which I seldom use
@ -11,60 +38,13 @@
inhibit-startup-echo-area-message user-login-name ; read the docstring
inhibit-startup-buffer-menu t)
;; I do not use those graphical elements by default, but I do enable
;; them from time-to-time for testing purposes or to demonstrate
;; something. NEVER tell a beginner to disable any of these. They
;; are helpful.
(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)
(defvar prot-laptop-p (null (directory-empty-p "/sys/class/power_supply/"))
"When non-nil, we assume to be working on a laptop.")
(when prot-laptop-p
(add-hook 'window-size-change-functions #'frame-hide-title-bar-when-maximized))
(defvar prot-pgtk-p (string-match-p "PGTK" system-configuration-features)
"When non-nil, this is a build --with-pgtk.
PGTK is the Wayland-specific build of Emacs.")
(setq initial-frame-alist `((horizontal-scroll-bars . nil)
(menu-bar-lines . 0)
(tool-bar-lines . 0)
(vertical-scroll-bars . nil)
(scroll-bar-width . ,(if prot-pgtk-p 12 6))
(width . (text-pixels . 800))
(height . (text-pixels . 900))
,@(unless prot-pgtk-p
(list '(undecorated . t)))
(border-width . 0)
,@(when prot-laptop-p
(list '(fullscreen . maximized)))))
;; Do it again after init so that any intermediate changes are not
;; retained. Note that we cannot rely on setting this to
;; `initial-frame-alist' as that may change in the meantime. We
;; explicitly set the value to be certain of the outcome. This does
;; not inhibit other programs from modifying the list, though I would
;; consider it undesirable if they were touching these specific
;; settings.
(add-hook 'after-init-hook (lambda ()
(setq default-frame-alist `((horizontal-scroll-bars . nil)
(menu-bar-lines . 0)
(tool-bar-lines . 0)
(vertical-scroll-bars . nil)
(scroll-bar-width . (if prot-pgtk-p 12 6))
(width . (text-pixels . 800))
(height . (text-pixels . 900))
,@(unless prot-pgtk-p
(list '(undecorated . t)))
(border-width . 0)
,@(when prot-laptop-p
(list '(fullscreen . maximized)))))))
(defun prot-emacs-no-minibuffer-scroll-bar (frame)
"Remove the minibuffer scroll bars from FRAME."
(when scroll-bar-mode
(set-window-scroll-bars (minibuffer-window frame) nil nil nil nil :persistent)))
(add-hook 'after-make-frame-functions #'prot-emacs-no-minibuffer-scroll-bar)
(tool-bar-mode -1)
;; Temporarily increase the garbage collection threshold. These
;; changes help shave off about half a second of startup time. The
@ -85,8 +65,8 @@ PGTK is the Wayland-specific build of Emacs.")
(add-hook 'emacs-startup-hook
(lambda ()
(setq gc-cons-threshold (* 100 100 8)
gc-cons-percentage 0.1
(setq gc-cons-threshold (* 1024 1024 20)
gc-cons-percentage 0.2
file-name-handler-alist prot-emacs--file-name-handler-alist
vc-handled-backends prot-emacs--vc-handled-backends)))
@ -97,6 +77,56 @@ PGTK is the Wayland-specific build of Emacs.")
;; packages to work with, requiring a `package-refresh-contents'.
(setq package-enable-at-startup t)
(setq user-lisp-directory (locate-user-emacs-file "prot-lisp/"))
;;;; General theme code
(defun prot-emacs-theme-gsettings-dark-p ()
"Return non-nil if gsettings (GNOME) has a dark theme.
Return nil if the DESKTOP_SESSION is either bspwm or
herbstluftwm, per the configuration of my dotfiles. Also check
the `delight' shell script."
(prot-emacs-with-desktop-session
(string-match-p
"dark"
(shell-command-to-string "gsettings get org.gnome.desktop.interface color-scheme"))))
(defun prot-emacs-theme-twm-dark-p ()
"Return non-nil if my custom setup has a dark theme.
I place a file in ~/.config/prot-xtwm-active-theme which contains
a single word describing my system-wide theme. This is part of
my dotfiles. Check my `delight' shell script for more."
(when-let ((file "~/.config/prot-xtwm-active-theme")
((file-exists-p file)))
(string-match-p
"dark"
(with-temp-buffer
(insert-file-contents file)
(buffer-string)))))
(defun prot-emacs-theme-environment-dark-p ()
"Return non-nil if environment theme is dark."
(or (prot-emacs-theme-twm-dark-p)
(prot-emacs-theme-gsettings-dark-p)))
(defun prot-emacs-re-enable-frame-theme (_frame)
"Re-enable active theme, if any, upon FRAME creation.
Add this to `after-make-frame-functions' so that new frames do
not retain the generic background set by the function
`prot-emacs-avoid-initial-flash-of-light'."
(when-let ((theme (car custom-enabled-themes)))
(enable-theme theme)))
;; NOTE 2023-02-05: The reason the following works is because (i) the
;; `mode-line-format' is specified again and (ii) the
;; `prot-emacs-theme-gsettings-dark-p' will load a dark theme.
(defun prot-emacs-avoid-initial-flash-of-light ()
"Avoid flash of light when starting Emacs, if needed.
New frames are instructed to call `prot-emacs-re-enable-frame-theme'."
(when (prot-emacs-theme-environment-dark-p)
(setq mode-line-format nil)
(set-face-attribute 'default nil :background "#000000" :foreground "#ffffff")
(set-face-attribute 'mode-line nil :background "#000000" :foreground "#ffffff" :box 'unspecified)
(add-hook 'after-make-frame-functions #'prot-emacs-re-enable-frame-theme)))
(prot-emacs-avoid-initial-flash-of-light)
(add-hook 'after-init-hook (lambda () (set-frame-name "home")))

View file

@ -10,9 +10,9 @@ directory as the init.el."
(defcustom prot-emacs-load-theme-family 'modus
"Set of themes to load.
Valid values are the symbols `doric', `ef', `modus', and `standard',
which reference the `doric-themes', `ef-themes', `modus-themes', and
`standard-themes', respectively.
Valid values are the symbols `ef', `modus', and `standard', which
reference the `ef-themes', `modus-themes', and `standard-themes',
respectively.
A nil value does not load any of the above (use Emacs without a
theme).
@ -22,57 +22,70 @@ file. If that file exists in the Emacs directory, it is loaded
before all other modules of my setup."
:group 'prot-emacs
:type '(choice :tag "Set of themes to load" :value modus
(const :tag "The `doric-themes' module" doric)
(const :tag "The `ef-themes' module" ef)
(const :tag "The `modus-themes' module" modus)
(const :tag "The `standard-themes' module" standard)
(const :tag "Do not load a theme module" nil)))
(defcustom prot-emacs-completion-ui 'vertico
"Choose minibuffer completion UI between `mct' or `vertico'.
If the value is nil, the default completion user interface is
used. On Emacs 30, this is close the experience with `mct'.
"Choose minibuffer completion UI between `mct' or `vertico'
This user option must be set in the `prot-emacs-pre-custom.el'
file. If that file exists in the Emacs directory, it is loaded
before all other modules of my setup.."
:group 'prot-emacs
:type '(choice :tag "Minibuffer user interface"
(const :tag "The `mct' module" mct)
(const :tag "The `vertico' module" vertico)))
(defcustom prot-emacs-load-evil nil
"When non-nil, load Vim style key bindings as well as `devil-mode'.
This user option must be set in the `prot-emacs-pre-custom.el'
file. If that file exists in the Emacs directory, it is loaded
before all other modules of my setup."
:group 'prot-emacs
:type 'boolean)
(defcustom prot-emacs-load-which-key nil
"When non-nil, display key binding hints after a short delay.
This user option must be set in the `prot-emacs-pre-custom.el'
file. If that file exists in the Emacs directory, it is loaded
before all other modules of my setup."
:group 'prot-emacs
:type 'boolean)
(defcustom prot-emacs-load-icons nil
"When non-nil, enable iconography in various contexts.
This installs and uses the `nerd-icons' package and its variants.
NOTE that you still need to invoke `nerd-icons-install-fonts'
manually to first get the icon files.
This user option must be set in the `prot-emacs-pre-custom.el'
file. If that file exists in the Emacs directory, it is loaded
before all other modules of my setup."
:group 'prot-emacs
:type '(choice :tag "Minibuffer user interface"
(const :tag "Default user interface" nil)
(const :tag "The `mct' module" mct)
(const :tag "The `vertico' module" vertico)))
(defcustom prot-emacs-completion-extras t
"When non-nil load extras for minibuffer completion.
These include the packages `marginalia', `consult', `corfu',
`orderless', and `embark'."
:group 'prot-emacs
:type 'boolean)
(defcustom prot-emacs-completion-in-buffer 'corfu
"Front-end for text completion in the buffer.
When the value is either `corfu' or `company', use the package of the
given name. When the value is `completion-preview' use the built-in
`completion-preview-mode'. When the value is `corfu-completion-preview',
then use the `corfu' package and `completion-preview-mode' in tandem.
(defcustom prot-emacs-omit-packages nil
"List of package names to not load.
This instructs the relevant macros to not `require' the given
package. In the case of `prot-emacs-elpa-package', the package
will not be installed if it is not already available on the
system.
Use the generic completions buffer if any other value is set."
This user option must be set in the `prot-emacs-pre-custom.el'
file. If that file exists in the Emacs directory, it is loaded
before all other modules of my setup."
:group 'prot-emacs
:type '(choice
(const :tag "Generic completions buffer" nil)
(const :tag "Generic completions buffer" t)
(const :tag "The `corfu' package" corfu)
(const :tag "The `company' package" company)
(const :tag "The built-in `completion-preview-mode'" completion-preview)
(const :tag "Corfu and `completion-preview-mode'" corfu-completion-preview)))
:type '(repeat symbol))
(setq make-backup-files nil)
(setq backup-inhibited nil) ; Not sure if needed, given `make-backup-files'
(setq create-lockfiles nil)
;; Make native compilation silent.
;; Make native compilation silent and prune its cache.
(when (native-comp-available-p)
(setq native-comp-async-report-warnings-errors 'silent))
(setq native-comp-async-report-warnings-errors 'silent) ; Emacs 28 with native compilation
(setq native-compile-prune-cache t)) ; Emacs 29
;; Disable the damn thing by making it disposable.
(setq custom-file (make-temp-file "emacs-custom-"))
@ -84,7 +97,7 @@ Use the generic completions buffer if any other value is set."
(mapc
(lambda (command)
(put command 'disabled nil))
'(list-timers narrow-to-region narrow-to-page upcase-region downcase-region diff-restrict-view))
'(list-timers narrow-to-region narrow-to-page upcase-region downcase-region))
;; And disable these
(mapc
@ -92,14 +105,8 @@ Use the generic completions buffer if any other value is set."
(put command 'disabled t))
'(eshell project-eshell overwrite-mode iconify-frame diary))
;; Always start with *scratch*
(setq initial-buffer-choice t)
(setq initial-major-mode 'lisp-interaction-mode)
(setq initial-scratch-message
(format ";; This is `%s'. Type `%s' to evaluate and print results.\n\n"
'lisp-interaction-mode
(propertize
(substitute-command-keys "\\<lisp-interaction-mode-map>\\[eval-print-last-sexp]")
'face 'help-key-binding)))
(mapc
(lambda (string)
@ -136,30 +143,17 @@ Use the generic completions buffer if any other value is set."
'(agitate
altcaps
beframe
buffer-to-pdf
consult-denote
cursory
denote
denote-journal
denote-markdown
denote-merge
denote-org
denote-sequence
denote-silo
dired-preview
doric-themes
ef-themes
fontaine
gnome-accent-theme-switcher
institution-calendar
lin
logos
mct
modus-themes
notmuch-indicator
oxford-calendar
pulsar
show-font
spacious-padding
standard-themes
substitute
@ -170,139 +164,149 @@ Use the generic completions buffer if any other value is set."
;; Also read: <https://protesilaos.com/codelog/2022-05-13-emacs-elpa-devel/>
(setq package-pinned-packages
(mapcar
(lambda (package)
(cons package "gnu-elpa-devel"))
prot-emacs-my-packages))
;; These are for Emacs 31.
(setq package-review-policy
(delq nil
(append
(mapcar
(lambda (package)
(list 'not 'package package))
prot-emacs-my-packages)
(mapcar
(lambda (archive)
(let ((archive-name (car archive)))
(unless (string= archive-name "gnu-elpa-devel")
(cons 'archive archive-name))))
package-archives))))
(setq package-review-diff-command
(cons diff-command
'("-u"
"-x" "'*.elc'"
"-x" "'*-autoloads.el'"
"-x" "'*-pkg.el'"
"-x" "'*.info'"
"-x" "'*.texi'"
"-x" "'*.txt'"
"-x" "'*.md'"
"-x" "'*.org'")))
`(,@(mapcar
(lambda (package)
(cons package "gnu-elpa-devel"))
prot-emacs-my-packages)))
(setq custom-safe-themes t)
(defmacro prot-emacs-comment (&rest body)
"Determine what to do with BODY.
(defun prot-emacs-package-install (package &optional method)
"Install PACKAGE with optional METHOD.
If BODY contains an unquoted plist of the form (:eval t) then
return BODY inside a `progn'.
If METHOD is nil or the `builtin' symbol, PACKAGE is not
installed as it is considered part of Emacs.
Otherwise, do nothing with BODY and return nil, with no side
effects."
(declare (indent defun))
(let ((eval))
If METHOD is a string, it must be a URL pointing to the version
controlled repository of PACKAGE. Installation is done with
`package-vc-install'.
If METHOD is a quoted list, it must have a form accepted by
`package-vc-install' such as:
\\='(denote :url \"https://git.sr.ht/~protesilaos/denote\" :branch \"main\")
If METHOD is any other non-nil value, install PACKAGE using
`package-install'."
(unless (or (eq method 'builtin) (null method))
(unless (package-installed-p package)
(when (or (stringp method) (listp method))
(package-vc-install method))
(unless package-archive-contents
(package-refresh-contents))
(package-install package))))
(defvar prot-emacs-loaded-packages nil)
(defmacro prot-emacs-package (package &rest body)
"Require PACKAGE with BODY configurations.
PACKAGE is an unquoted symbol that is passed to `require'. It
thus conforms with `featurep'.
BODY consists of ordinary Lisp expressions. There are,
nevertheless, two unquoted plists that are treated specially:
1. (:install METHOD)
2. (:delay NUMBER)
These plists can be anywhere in BODY and are not part of its
final expansion.
The :install property is the argument passed to
`prot-emacs-package-install' and has the meaning of METHOD
described therein.
The :delay property makes the evaluation of PACKAGE with the
expanded BODY happen with `run-with-timer'.
Also see `prot-emacs-configure'."
(declare (indent 1))
(unless (memq package prot-emacs-omit-packages)
(let (install delay)
(dolist (element body)
(when (plistp element)
(pcase (car element)
(:install (setq install (cdr element)
body (delq element body)))
(:delay (setq delay (cadr element)
body (delq element body))))))
(let ((common `(,(when install
`(prot-emacs-package-install ',package ,@install))
(require ',package)
(add-to-list 'prot-emacs-loaded-packages ',package)
,@body
;; (message "Prot Emacs loaded package: %s" ',package)
)))
(cond
((featurep package)
`(progn ,@body))
(delay
`(run-with-timer ,delay nil (lambda () ,@(delq nil common))))
(t
`(progn ,@(delq nil common))))))))
;; Samples of `prot-emacs-package' (expand them with `pp-macroexpand-last-sexp').
;; (prot-emacs-package denote
;; (setq denote-directory "path/to/dir")
;; (define-key global-map (kbd "C-c n") #'denote)
;; (:install '(denote . (:url "https://git.sr.ht/~protesilaos/denote" :branch "main")))
;; (:delay 5)
;; (setq denote-file-type nil))
;;
;; (prot-emacs-package denote
;; (setq denote-directory "path/to/dir")
;; (define-key global-map (kbd "C-c n") #'denote)
;; (:install "https://git.sr.ht/~protesilaos/denote")
;; (:delay 5)
;; (setq denote-file-type nil))
;;
;; (prot-emacs-package denote
;; (:delay 5)
;; (setq denote-directory "path/to/dir")
;; (define-key global-map (kbd "C-c n") #'denote)
;; (:install "https://git.sr.ht/~protesilaos/denote")
;; (setq denote-file-type nil))
;;
;; (prot-emacs-package denote
;; (:install "https://git.sr.ht/~protesilaos/denote")
;; (:delay 5)
;; (setq denote-directory "path/to/dir")
;; (define-key global-map (kbd "C-c n") #'denote)
;; (setq denote-file-type nil))
;;
;; (prot-emacs-package denote
;; (:delay 5)
;; (setq denote-directory "path/to/dir")
;; (define-key global-map (kbd "C-c n") #'denote)
;; (setq denote-file-type nil))
;;
;; (prot-emacs-package denote
;; (setq denote-directory "path/to/dir")
;; (define-key global-map (kbd "C-c n") #'denote)
;; (setq denote-file-type nil))
(defmacro prot-emacs-configure (&rest body)
"Evaluate BODY as a `progn'.
BODY consists of ordinary Lisp expressions. The sole exception
is an unquoted plist of the form (:delay NUMBER) which evaluates
BODY with NUMBER seconds of `run-with-timer'.
Note that `prot-emacs-configure' does not try to autoload
anything. Use it only for forms that evaluate regardless.
Also see `prot-emacs-package'."
(declare (indent 0))
(let (delay)
(dolist (element body)
(when-let* (((plistp element))
(key (car element))
((eq key :eval))
(val (cadr element)))
(setq eval val
body (delq element body))))
(when eval `(progn ,@body))))
;; Sample use of `prot-emacs-comment'. The function
;; `prot-emacs-insert-comment-macro' is never evaluated.
(prot-emacs-comment
(defun prot-emacs-insert-comment-macro (beg end)
"Wrap region between BEG and END in `prot-emacs-comment'."
(interactive "r")
(if (region-active-p)
(let ((text (buffer-substring beg end)))
(delete-region beg end)
(insert (format "(prot-emacs-comment\n%s)" text))
(indent-region beg end))
(user-error "No active region; will not insert `prot-emacs-comment' here"))))
(defmacro prot-emacs-install (package)
"Install PACKAGE, unless it is installed."
(declare (indent 0))
(unless (symbolp package)
(error "The package `%s' is not a symbol" package))
`(unless (package-installed-p ',package)
(condition-case-unless-debug nil
(progn
(unless package-archive-contents
(package-refresh-contents))
(package-install ',package))
(error (message "Cannot install `%s'; try `M-x package-refresh-contents' first" ',package)))))
(defmacro prot-emacs-install-vc (package &rest vc-args)
"Install PACKAGE with VC-ARGS, unless it is installed."
(declare (indent 0))
(unless (symbolp package)
(error "The package `%s' is not a symbol" package))
`(progn
(unless (package-installed-p ',package)
(condition-case-unless-debug nil
,(cond
((length= vc-args 0)
`(error "VC-ARGS must be 1 or more: `%S'" ',vc-args))
((length> vc-args 1)
`(apply 'package-vc-install ,(append (list package) vc-args)))
(t
`(funcall 'package-vc-install ,(car vc-args))))
(error (message "Cannot install `%s' using VC-ARGS `%S'" ',package ',vc-args))))))
(defmacro prot-emacs-hook (hooks functions &optional remove after)
"For each HOOKS `add-hook' the FUNCTIONS.
With optional REMOVE as non-nil, then `remove-hook' the FUNCTIONS from
HOOKS.
With optional AFTER as the unquoted symbol of a feature, do so after the
given feature is available."
(declare (indent 0))
(cond
((symbolp hooks)
(setq hooks (list hooks)))
((not (proper-list-p hooks))
(error "The hooks are not a list: `%S'" hooks)))
(cond
((symbolp functions)
(setq functions (list functions)))
((not (proper-list-p functions))
(error "The functions are not a list: `%S'" functions)))
(let* ((fn (if remove 'remove-hook 'add-hook))
(body (mapcar
(lambda (h)
(mapcar
(lambda (f) `(,fn ',h #',f))
functions))
hooks))
(hooks nil))
(dolist (element body)
(dolist (hook element)
(push hook hooks)))
(setq hooks (nreverse hooks))
(cond
(after
`(with-eval-after-load ',after ,@hooks))
((length> hooks 1)
`(progn ,@hooks))
(t
(car hooks)))))
(when (plistp element)
(pcase (car element)
(:delay (setq delay (cadr element)
body (delq element body))))))
(if delay
`(run-with-timer ,delay nil (lambda () ,@body))
`(progn ,@body))))
(defmacro prot-emacs-keybind (keymap &rest definitions)
"Expand key binding DEFINITIONS for the given KEYMAP.
@ -313,8 +317,8 @@ DEFINITIONS is a sequence of string and command pairs."
(let ((keys (seq-filter #'stringp definitions))
;; We do accept nil as a definition: it unsets the given key.
(commands (seq-remove #'stringp definitions)))
`(when-let* (((keymapp ,keymap))
(map ,keymap))
`(when-let (((keymapp ,keymap))
(map ,keymap))
,@(mapcar
(lambda (pair)
(let* ((key (car pair))
@ -333,67 +337,57 @@ DEFINITIONS is a sequence of string and command pairs."
;; "C-c b" beframe-prefix-map
;; "C-x k" #'kill-buffer)
(defmacro prot-emacs-autoload (functions file)
"Declare autoloads for FUNCTIONS for FILE."
(declare (indent 0))
(when (symbolp functions)
(setq functions (list functions)))
(unless (listp functions)
(error "The functions must be a list or symbol: %S" functions))
(unless (stringp file)
(error "The file must be a string: %S" file))
(if (length> functions 1)
`(progn ,@(mapcar (lambda (f) `(autoload #',f ,file)) functions))
`(autoload #',(car functions) ,file)))
(defmacro prot-emacs-abbrev (table &rest definitions)
"Expand abbrev DEFINITIONS for the given TABLE.
DEFINITIONS is a sequence of (i) string pairs mapping the
abbreviation to its expansion or (ii) a string and symbol pair
making an abbreviation to a function."
DEFINITIONS is a sequence of string pairs mapping the
abbreviation to its expansion."
(declare (indent 1))
(unless (zerop (% (length definitions) 2))
(error "Uneven number of key+command pairs"))
`(if (abbrev-table-p ,table)
(progn
,@(mapcar
(lambda (pair)
(let ((abbrev (nth 0 pair))
(expansion (nth 1 pair)))
(if (stringp expansion)
`(define-abbrev ,table ,abbrev ,expansion)
`(define-abbrev ,table ,abbrev "" ,expansion))))
(seq-split definitions 2))
;; Allow abbrevs with a prefix colon, semicolon, or underscore. I demonstrated
;; this here: <https://protesilaos.com/codelog/2024-02-03-emacs-abbrev-mode/>.
(abbrev-table-put ,table :regexp "\\(?:^\\|[\t\s]+\\)\\(?1:[:;_].*\\|.*\\)"))
(error "%s is not an abbrev table" ,table)))
`(when-let (((abbrev-table-p ,table))
(table ,table))
,@(mapcar
(lambda (pair)
(when-let ((abbrev (car pair))
(expansion (cadr pair)))
`(define-abbrev table ,abbrev ,expansion)))
(seq-split definitions 2))))
(defmacro prot-emacs-configure (&rest body)
"Evaluate BODY and catch any errors."
(declare (indent 0))
`(condition-case err
(progn ,@body)
((error user-error quit)
(message "Failed to configure package starting with `%S' because of `%S'" (car ',body) (cdr err)))))
(defun prot-emacs-return-loaded-packages ()
"Return a list of all loaded packages.
Here packages include both `prot-emacs-loaded-packages' and
`package-activated-list'. The latter only covers what is found
in the `package-archives', whereas the former is for anything
that is expanded with the `prot-emacs-package' macro."
(delete-dups (append prot-emacs-loaded-packages package-activated-list)))
(defvar prot-emacs-package-form-regexp
"^(\\(prot-emacs-package\\|prot-emacs-keybind\\|prot-emacs-abbrev\\|require\\) +'?\\([0-9a-zA-Z-]+\\)"
"Regexp to add packages to `lisp-imenu-generic-expression'.")
(eval-after-load 'lisp-mode
`(add-to-list 'lisp-imenu-generic-expression
(list "Packages" ,prot-emacs-package-form-regexp 2)))
(defconst prot-emacs-font-lock-keywords
'(("(\\(prot-emacs-package\\)\\_>[ \t']*\\(\\(?:\\sw\\|\\s_\\)+\\)?"
(2 font-lock-constant-face nil t))
("(\\(prot-emacs-\\(keybind\\|abbrev\\)\\)\\_>[ \t']*\\(\\(\\sw\\|\\s_\\)+\\)?"
(3 font-lock-variable-name-face nil t))))
(font-lock-add-keywords 'emacs-lisp-mode prot-emacs-font-lock-keywords)
;; For those who use my dotfiles and need an easy way to write their
;; own extras on top of what I already load. The file must exist at
;; ~/.emacs.d/prot-emacs-pre-custom.el
;;
;; The purpose of this file is for the user to define their
;; preferences BEFORE loading any of the modules.
;; preferences BEFORE loading any of the modules. For example, the
;; user option `prot-emacs-omit-packages' lets the user specify which
;; packages not to load. Search for all `defcustom' forms in this
;; file for other obvious customisations.
(load (locate-user-emacs-file "prot-emacs-pre-custom.el") :no-error :no-message)
(defvar prot-display-graphic-p (display-graphic-p)
"When non-nil, the display is graphical.")
(defun prot-emacs-gnome-prefers-dark-p ()
"Return non-nil if GNOME color-scheme is set to dark."
(string-match-p
"dark"
(shell-command-to-string "gsettings get org.gnome.desktop.interface color-scheme")))
(require 'prot-emacs-theme)
(require 'prot-emacs-essentials)
(require 'prot-emacs-modeline)
@ -406,13 +400,20 @@ making an abbreviation to a function."
(require 'prot-emacs-langs)
(require 'prot-emacs-email)
(require 'prot-emacs-web)
(require 'prot-emacs-which-key)
(require 'prot-emacs-icons)
(when prot-emacs-load-which-key
(require 'prot-emacs-which-key))
(when prot-emacs-load-icons
(require 'prot-emacs-icons))
;; We load it last to override any other keys.
(when prot-emacs-load-evil
(require 'prot-emacs-evil))
;; For those who use my dotfiles and need an easy way to write their
;; own extras on top of what I already load. The file must exist at
;; ~/.emacs.d/prot-emacs-post-custom.el
;;
;; The purpose of the "post customisations" is to evaluate arbitrary
;; code AFTER loading all my configurations.
;; The purpose of the "post customisations" is to make tweaks to what
;; I already define, such as to change the default theme. See above
;; for the `prot-emacs-pre-custom.el' to make changes BEFORE loading
;; any of my other configurations.
(load (locate-user-emacs-file "prot-emacs-post-custom.el") :no-error :no-message)

View file

@ -1,158 +1,136 @@
;;; General minibuffer settings
(prot-emacs-configure
;;;; Completion styles
(setq completion-styles '(basic substring initials partial-completion flex)) ; also see `completion-category-overrides'
(setq completion-flex-nospace t)
(setq completion-pcm-leading-wildcard nil) ; Emacs 31
(with-eval-after-load 'orderless
(setq completion-styles (append completion-styles '(orderless)))))
;;;; Completion category overrides
(prot-emacs-configure
;; Reset all the per-category defaults so that (i) we use the
;; standard `completion-styles' and (ii) can specify our own styles
;; in the `completion-category-overrides' without having to
;; explicitly override everything.
(:delay 1)
;;;; Minibuffer configurations
(setq completion-styles '(basic substring initials flex orderless)) ; also see `completion-category-overrides'
(setq completion-category-defaults nil)
(prot-minibuffer-missing-categories-mode 1)
;; A list of known completion categories:
;;
;; - `bookmark'
;; - `buffer'
;; - `charset'
;; - `coding-system'
;; - `color'
;; - `command' (e.g. `M-x')
;; - `customize-group'
;; - `environment-variable'
;; - `expression'
;; - `face'
;; - `file'
;; - `function' (the `describe-function' command bound to `C-h f')
;; - `info-menu'
;; - `imenu'
;; - `input-method'
;; - `kill-ring'
;; - `library'
;; - `minor-mode'
;; - `multi-category'
;; - `package'
;; - `project-file'
;; - `symbol' (the `describe-symbol' command bound to `C-h o')
;; - `theme'
;; - `unicode-name' (the `insert-char' command bound to `C-x 8 RET')
;; - `variable' (the `describe-variable' command bound to `C-h v')
;;
;; From the `consult' package:
;;
;; - `consult-grep'
;; - `consult-isearch'
;; - `consult-kmacro'
;; - `consult-location'
;;
;; From the `embark' package:
;;
;; - `embark-keybinding'
;;
(setq completion-category-overrides
;; NOTE 2021-10-25: I am adding `basic' because it works better as a
;; default for some contexts. Read:
;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=50387>.
;;
;; `partial-completion' is a killer app for files, because it
;; can expand ~/.l/s/fo to ~/.local/share/fonts.
;;
;; If `basic' cannot match my current input, Emacs tries the
;; next completion style in the given order. In other words,
;; `orderless' kicks in as soon as I input a space or one of its
;; style dispatcher characters.
'((file (styles . (basic partial-completion orderless)))
(bookmark (styles . (basic substring)))
(library (styles . (basic substring)))
(embark-keybinding (styles . (basic substring)))
(imenu (styles . (basic substring orderless)))
(consult-location (styles . (basic substring orderless)))
(kill-ring (styles . (emacs22 orderless)))
(eglot (styles . (emacs22 substring orderless)))))
;; NOTE 2025-12-02: The `eager-display' and `eager-update' are part of Emacs 31.
(let* ((eager-both '((eager-display . t)
(eager-update . t)))
(eager-both-no-sort (append eager-both (list (cons 'display-sort-function #'identity))))
(eager-update-only '((eager-display . nil)
(eager-update . t)))
(eager-update-only-no-sort (append eager-update-only (list (cons 'display-sort-function #'identity)))))
(setq completion-category-overrides
`((file . (,@eager-update-only
(styles . (partial-completion))
(group-function . ,#'prot-minibuffer-file-group)
(affixation-function . ,#'prot-minibuffer-file-affixate)
(display-sort-function . ,#'prot-minibuffer-file-sort)))
(buffer . (,@eager-update-only
(affixation-function . ,#'prot-minibuffer-buffer-affixate)))
(project-file . (,@eager-update-only
(group-function . ,#'prot-minibuffer-file-group)
(affixation-function . ,#'prot-minibuffer-file-affixate)))
(prot-minibuffer-library . (,@eager-update-only
(annotation-function . ,#'prot-minibuffer-library-annotate)
(display-sort-function . ,#'prot-minibuffer-library-sort)))
(bookmark . (,@eager-update-only
(affixation-function . ,#'prot-minibuffer-bookmark-affixate)))
(command . (,@eager-update-only
(affixation-function . nil) ; so that the `annotation-function' can take effect
(annotation-function . ,#'prot-minibuffer-command-annotate)))
(prot-minibuffer-input-method . (,@eager-update-only
(group-function . ,#'prot-minibuffer-input-method-group)
(display-sort-function . ,#'prot-minibuffer-input-method-sort)))
(symbol-help . ,eager-update-only)
(denote-file . ,eager-update-only)
(cape-abbrev . ,eager-update-only)
(cape-dabbrev . ,eager-update-only)
(cape-dict . ,eager-update-only)
(cape-emoji . ,eager-update-only)
(cape-history . ,eager-update-only)
(cape-keyword . ,eager-update-only)
(cape-super . ,eager-update-only)
(tmr-timer . ,eager-both)
(prot-minibuffer-emoji . ,eager-update-only)
(theme . ,eager-update-only)
(unicode-name . ,eager-update-only)
(prot-minibuffer-pass . ,eager-update-only)
(imenu . ,eager-update-only-no-sort)
(consult-location . ,eager-update-only-no-sort)
(prot-minibuffer-kill-ring . ,eager-update-only-no-sort)))))
(setq completion-ignore-case t)
(setq read-buffer-completion-ignore-case t)
(setq read-file-name-completion-ignore-case t)
(setq-default case-fold-search t) ; For general regexp
;;; Orderless completion style (and prot-orderless.el)
(when prot-emacs-completion-extras
(prot-emacs-configure
(prot-emacs-install orderless)
(require 'orderless)
;; Remember to check my `completion-styles' and the
;; `completion-category-overrides'.
(setq orderless-matching-styles '(orderless-prefixes orderless-regexp))
(setq orderless-smart-case nil)
;; SPC should never complete: use it for `orderless' groups.
;; The `?' is a regexp construct.
(prot-emacs-keybind minibuffer-local-completion-map
"SPC" nil
"?" nil)))
(setq completion-ignore-case t)
(setq read-buffer-completion-ignore-case t)
(setq-default case-fold-search t) ; For general regexp
(setq read-file-name-completion-ignore-case t)
(setq minibuffer-history-case-insensitive-variables t)
(prot-emacs-configure
(setq read-minibuffer-restore-windows nil)
(setq enable-recursive-minibuffers t) ; Emacs 28
(minibuffer-depth-indicate-mode 1))
(prot-emacs-configure
(setq minibuffer-default-prompt-format " [%s]") ; Emacs 29
(minibuffer-electric-default-mode 1))
(prot-emacs-configure
(setq enable-recursive-minibuffers t)
;; Allow Emacs to resize mini windows, otherwise this does not work:
;; (setq org-use-fast-todo-selection 'expert)
(setq resize-mini-windows t)
(setq minibuffer-eldef-shorten-default t)
(setq read-answer-short t) ; also check `use-short-answers' for Emacs28
(setq echo-keystrokes 0.25)
(setq kill-ring-max 60) ; Keep it small
(setq kill-ring-max 60) ; Keep it small
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
;; Do not allow the cursor to move inside the minibuffer prompt. I
;; got this from the documentation of Daniel Mendler's Vertico
;; package: <https://github.com/minad/vertico>.
(setq minibuffer-prompt-properties
'(read-only t cursor-intangible t face minibuffer-prompt))
(setq crm-prompt (format "%s %%p" (propertize "[%d]" 'face 'shadow))) ; Emacs 31
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
(file-name-shadow-mode 1))
;; Add prompt indicator to `completing-read-multiple'. We display
;; [`completing-read-multiple': <separator>], e.g.,
;; [`completing-read-multiple': ,] if the separator is a comma. This
;; is adapted from the README of the `vertico' package by Daniel
;; Mendler. I made some small tweaks to propertize the segments of
;; the prompt.
(defun crm-indicator (args)
(cons (format "[`completing-read-multiple': %s] %s"
(propertize
(replace-regexp-in-string
"\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
crm-separator)
'face 'error)
(car args))
(cdr args)))
(prot-emacs-configure
(prot-emacs-hook
(completion-list-mode-hook minibuffer-setup-hook)
prot-common-truncate-lines-silently)
(advice-add #'completing-read-multiple :filter-args #'crm-indicator)
(setq completions-group-format
(concat
(propertize (make-string 20 ? ) 'face 'completions-group-separator)
(propertize " %s " 'face 'completions-group-title)
(propertize " " 'face 'completions-group-separator 'display '(space :align-to right))))
(unless prot-emacs-completion-ui
(prot-minibuffer-completions-mode 1)
(prot-emacs-keybind completion-in-region-mode-map
"M-n" #'minibuffer-next-completion
"M-p" #'minibuffer-previous-completion)
(prot-emacs-keybind minibuffer-local-completion-map
"<down>" #'minibuffer-next-line-completion
"<up>" #'minibuffer-previous-line-completion
"C-h C-h" #'prot-minibuffer-completions-describe-at-point ; overrides `help-for-help'
"C-<tab>" #'prot-minibuffer-choose-completion-no-exit
"RET" #'prot-minibuffer-choose-completion-exit)
(prot-emacs-keybind completion-list-mode-map
"C-h C-h" #'prot-minibuffer-completions-describe-at-point ; overrides `help-for-help'
"C-<tab>" #'prot-minibuffer-choose-completion-no-exit
"RET" #'prot-minibuffer-choose-completion-dwim)))
;; Settings for the default completion UI. These do not come into
;; effect unless `prot-emacs-completion-ui' is nil or when not using
;; any package for in-buffer completion.
(setq completion-show-help nil)
(setq completion-auto-help t)
(setq completion-auto-select nil)
(setq completions-detailed t)
(setq completion-show-inline-help nil)
(setq completions-max-height 6)
(setq completions-header-format
(propertize "%s candidates:\n" 'face 'font-lock-comment-face))
(setq completions-highlight-face 'completions-highlight)
(file-name-shadow-mode 1)
(minibuffer-depth-indicate-mode 1)
(minibuffer-electric-default-mode 1)
;;;; `savehist' (minibuffer and related histories)
(prot-emacs-configure
(setq savehist-file (locate-user-emacs-file "savehist"))
(setq history-length 100)
(setq history-length 500)
(setq history-delete-duplicates t)
(setq savehist-save-minibuffer-history t)
(with-eval-after-load 'savehist
(add-to-list 'savehist-additional-variables 'kill-ring))
(savehist-mode 1))
(setq savehist-additional-variables '(register-alist kill-ring))
(savehist-mode 1)
(prot-emacs-configure
;;;; `dabbrev' (dynamic word completion (dynamic abbreviations))
(setq dabbrev-abbrev-char-regexp "\\sw\\|\\s_")
(setq dabbrev-abbrev-skip-leading-regexp "[$*/=~']")
@ -163,46 +141,23 @@
(setq dabbrev-check-other-buffers t)
(setq dabbrev-eliminate-newlines t)
(setq dabbrev-upcase-means-case-search t)
(setq dabbrev-ignored-buffer-modes '(archive-mode image-mode docview-mode pdf-view-mode)))
(setq dabbrev-ignored-buffer-modes
'(archive-mode image-mode docview-mode pdf-view-mode))
;;;; `abbrev' (Abbreviations, else Abbrevs)
(prot-emacs-configure
(require 'prot-abbrev)
(setq abbrev-file-name (locate-user-emacs-file "abbrevs"))
(setq only-global-abbrevs nil)
(define-key global-map (kbd "C-x a u") #'unexpand-abbrev)
;; Because the *scratch* buffer is produced before we load this, we
;; have to explicitly activate the mode there.
(when-let* ((scratch (get-buffer "*scratch*")))
(with-current-buffer scratch
(abbrev-mode 1)))
;; By default, abbrev asks for confirmation on whether to use
;; `abbrev-file-name' to save abbrevations. I do not need that, nor
;; do I want it.
(remove-hook 'save-some-buffers-functions #'abbrev--possibly-save)
(prot-emacs-hook
( text-mode-hook prog-mode-hook git-commit-mode-hook
vc-git-log-edit-mode-hook message-mode-hook)
abbrev-mode)
(prot-emacs-abbrev global-abbrev-table
";web" "https://protesilaos.com"
";git" "https://github.com/protesilaos"
";hub" "https://github.com/protesilaos"
";clone" "git@github.com/protesilaos/"
";lab" "https://gitlab.com/protesilaos"
";time" #'prot-abbrev-current-time
";date" #'prot-abbrev-current-date
";jitsi" #'prot-abbrev-jitsi-link)
"meweb" "https://protesilaos.com"
"megit" "https://git.sr.ht/~protesilaos"
"mehub" "https://github.com/protesilaos"
"melab" "https://gitlab.com/protesilaos"
"medrive" "hyper://5cr7mxac8o8aymun698736tayrh1h4kbqf359cfk57swjke716gy/")
(prot-emacs-abbrev text-mode-abbrev-table
"asciidoc" "AsciiDoc"
"auctex" "AUCTeX"
"cafe" "café"
"cliche" "cliché"
"clojurescript" "ClojureScript"
"emacsconf" "EmacsConf"
@ -217,250 +172,231 @@
"texmacs" "TeXmacs"
"typescript" "TypeScript"
"visavis" "vis-à-vis"
"deja" "déjà"
"voila" "voilà"
"youtube" "YouTube"
";up" "🙃"
";uni" "🦄"
";laugh" "🤣"
";smile" "😀"
";update" #'prot-abbrev-update-html)
(with-eval-after-load 'org
(prot-emacs-abbrev org-mode-abbrev-table
";dev" "{{{development-version}}}"
";key" #'prot-abbrev-org-macro-key
";cmd" #'prot-abbrev-org-macro-key-command))
"youtube" "YouTube")
(with-eval-after-load 'message
(prot-emacs-abbrev message-mode-abbrev-table
"bestregards" "Best regards,\nProtesilaos (or simply \"Prot\")"
"allthebest" "All the best,\nProtesilaos (or simply \"Prot\")"
"niceday" "Have a nice day,\nProtesilaos (or simply \"Prot\")"
"abest" "All the best,\nProt"
"bregards" "Best regards,\nProt"
"nday" "Have a nice day,\nProt"))
"bregards" "Best regards,\nProt"))
(with-eval-after-load 'markdown-mode
(prot-emacs-abbrev markdown-mode-abbrev-table
";vlog" "---
title: \"#\"
excerpt: \"#\"
layout: vlog
mediaid: \"#\"
---
"
";post" "---
title: \"#\"
excerpt: \"#\"
---
"
";poem" "---
title: \"#\"
excerpt: \"Just read the poem. No further comment.\"
---
"))
;; message-mode derives from text-mode, so we don't need a separate
;; hook for it.
(dolist (hook '(text-mode-hook prog-mode-hook git-commit-mode-hook))
(add-hook hook #'abbrev-mode))
(with-eval-after-load 'vc-git
(prot-emacs-abbrev vc-git-log-edit-mode-abbrev-table
";update" #'prot-abbrev-update-html)))
;; By default, abbrev asks for confirmation on whether to use
;; `abbrev-file-name' to save abbrevations. I do not need that, nor
;; do I want it.
(remove-hook 'save-some-buffers-functions #'abbrev--possibly-save)
(prot-emacs-keybind global-map
"M-/" #'dabbrev-expand
"C-x M-/" #'dabbrev-completion
"C-x a e" #'expand-abbrev ; default, just here for visibility
"C-x a u" #'unexpand-abbrev))
;;; Orderless completion style (and prot-orderless.el)
(prot-emacs-package orderless
(:install t)
(:delay 5)
(setq orderless-component-separator " +")
;; Remember to check my `completion-styles' and the
;; `completion-category-overrides'.
(setq orderless-matching-styles
'(orderless-prefixes orderless-regexp))
;; SPC should never complete: use it for `orderless' groups.
;; The `?' is a regexp construct.
(prot-emacs-keybind minibuffer-local-completion-map
"SPC" nil
"?" nil))
(prot-emacs-package prot-orderless
(setq orderless-style-dispatchers
'(prot-orderless-literal
prot-orderless-file-ext
prot-orderless-beg-or-end)))
;;; Corfu (in-buffer completion popup)
(when (or (eq prot-emacs-completion-in-buffer 'corfu)
(eq prot-emacs-completion-in-buffer 'corfu-completion-preview))
(prot-emacs-configure
(prot-emacs-install corfu)
(prot-emacs-package corfu
(:install t)
(:delay 5)
(global-corfu-mode 1)
(setq corfu-preview-current nil)
(setq corfu-min-width 20)
(setq corfu-popupinfo-delay '(1.25 . 0.5))
(corfu-popupinfo-mode 1) ; shows documentation after `corfu-popupinfo-delay'
(setq corfu-popupinfo-delay '(1.25 . 0.5))
(corfu-popupinfo-mode 1) ; shows documentation after `corfu-popupinfo-delay'
(define-key corfu-map (kbd "<tab>") #'corfu-complete)
(global-corfu-mode 1)
;; Sort by input history (no need to modify `corfu-sort-function').
(with-eval-after-load 'savehist
(corfu-history-mode 1)
(add-to-list 'savehist-additional-variables 'corfu-history))
;; I also have (setq tab-always-indent 'complete) for TAB to complete
;; when it does not need to perform an indentation change.
(define-key corfu-map (kbd "<tab>") #'corfu-complete)
;; Adapted from Corfu's manual.
(defun contrib/corfu-enable-always-in-minibuffer ()
"Enable Corfu in the minibuffer if MCT or Vertico is not active.
Useful for prompts such as `eval-expression' and `shell-command'."
(unless (or (bound-and-true-p vertico--input)
(bound-and-true-p mct--active))
(corfu-mode 1)))
;; Sort by input history (no need to modify `corfu-sort-function').
(with-eval-after-load 'savehist
(corfu-history-mode 1)
(add-to-list 'savehist-additional-variables 'corfu-history))))
(unless (eq prot-emacs-completion-in-buffer 'company)
(prot-emacs-configure
(prot-emacs-install cape)
(defun prot/cape-super-set-local (capfs &optional individual-capfs)
"Set `completion-at-point-functions' to current value plus CAPFS.
Treat CAPFS and the default value as a super CAPF. Then append the
INDIVIDUAL-CAPFS to the list."
(let* ((all-for-super (append completion-at-point-functions capfs))
(all-minus-global (delq t all-for-super))
(cape-super (apply #'cape-capf-super all-minus-global)))
(setq-local completion-at-point-functions (append (list cape-super) individual-capfs (list t)))))
(defun prot/cape-prog-setup ()
"Set up Cape for programming."
(prot/cape-super-set-local '(cape-dabbrev cape-abbrev) '(cape-file)))
(add-hook 'prog-mode-hook #'prot/cape-prog-setup)
(defun prot/cape-text-setup ()
"Set up Cape for prose."
(prot/cape-super-set-local '(cape-dict cape-dabbrev cape-abbrev cape-emoji) '(cape-file)))
(setq completion-at-point-functions '(cape-dabbrev cape-file))
(add-hook 'text-mode-hook #'prot/cape-text-setup)))
(when (eq prot-emacs-completion-in-buffer 'company)
(prot-emacs-configure
(prot-emacs-install company)
(prot-emacs-install company-statistics)
;; NOTE 2026-07-10: with the latest version of Company, (setq
;; tab-always-indent 'complete) is supported out-of-the-box, which
;; means that I can finally disable the automatic popup feature.
(setq company-idle-delay nil)
(setq company-tooltip-limit 6)
(setq company-tooltip-minimum-width 25)
(setq company-tooltip-align-annotations t)
(setq company-dabbrev-code-completion-styles t) ; use the `completion-styles'
(setq company-transformers '(company-sort-by-occurrence
company-sort-by-backend-importance
company-sort-prefer-same-case-prefix))
(setq company-frontends '(company-childframe-unless-just-one-frontend
company-preview-if-just-one-frontend
company-echo-metadata-frontend))
(setq company-backends '((company-capf company-dabbrev) company-files))
(define-advice company-show-doc-buffer (:around (&rest args) prot)
(let ((help-window-select nil)
(display-buffer-alist nil)
(display-buffer-overriding-action
'((display-buffer-reuse-mode-window display-buffer-below-selected)
(window-height . 0.3)
(post-command-select-window . nil))))
(apply args)))
(defun prot/company-prog-backends ()
"Set `company-backends' locally to my preferred value for `prog-mode' and derivatives."
(setq-local company-backends '((company-capf company-dabbrev company-abbrev) company-files)))
(defun prot/company-text-backends ()
"Set `company-backends' locally to my preferred value for `text-mode' and derivatives."
(setq-local company-backends '((company-dabbrev company-abbrev company-ispell company-capf) company-files)))
(prot-emacs-hook (prog-mode-hook comint-mode-hook) prot/company-prog-backends)
(prot-emacs-hook (prog-mode-hook text-mode-hook comint-mode-hook) company-mode)
(add-hook 'text-mode-hook #'prot/company-text-backends)
(with-eval-after-load 'company
(prot-emacs-keybind company-active-map
"C-s" #'company-filter-candidates
"<escape>" #'company-abort
"<tab>" #'company-complete-selection
"<up>" #'company-select-previous-or-abort
"<down>" #'company-select-next-or-abort)
(company-statistics-mode 1))))
(when (or (eq prot-emacs-completion-in-buffer 'completion-preview)
(eq prot-emacs-completion-in-buffer 'corfu-completion-preview))
(prot-emacs-configure
(setq completion-preview-exact-match-only nil)
(setq completion-preview-minimum-symbol-length 2)
(setq completion-preview-idle-delay nil)
(setq completion-preview-ignore-case t)
(prot-emacs-hook
(prog-mode-hook log-edit-mode-hook git-commit-mode-hook comint-mode-hook)
completion-preview-mode)
(with-eval-after-load 'completion-preview
(setq completion-preview-commands
(seq-remove
(lambda (command)
(memq command '(delete-backward-char backward-delete-char-untabify)))
completion-preview-commands))
(prot-emacs-keybind completion-preview-active-mode-map
"M-i" #'completion-preview-insert-word
"M-n" #'completion-preview-next-candidate
"M-p" #'completion-preview-prev-candidate
"M-<return>" #'completion-preview-insert
;; With TAB we effectively defer to another frontend to show
;; more completion candidates at once.
"<tab>" #'completion-preview-complete))
(with-eval-after-load 'org
(add-to-list 'completion-preview-commands #'org-self-insert-command))))
(add-hook 'minibuffer-setup-hook #'contrib/corfu-enable-always-in-minibuffer 1))
;;; Enhanced minibuffer commands (consult.el)
(when prot-emacs-completion-extras
(prot-emacs-configure
(prot-emacs-install consult)
(prot-emacs-package consult
(:install t)
(:delay 5)
(setq consult-line-numbers-widen t)
;; (setq completion-in-region-function #'consult-completion-in-region)
(setq consult-async-min-input 3)
(setq consult-async-input-debounce 0.5)
(setq consult-async-input-throttle 0.8)
(setq consult-narrow-key nil)
(setq register-preview-delay 0.8
register-preview-function #'consult-register-format)
(setq consult-find-args
(concat "find . -not ( "
"-path */.git* -prune "
"-or -path */.cache* -prune )"))
(setq consult-preview-key 'any)
(prot-emacs-keybind global-map
"M-g M-g" #'consult-goto-line
"M-s M-b" #'consult-buffer
"M-s M-f" #'consult-find
"M-s M-g" #'consult-grep
"M-s M-h" #'consult-history
"M-s M-i" #'consult-imenu
"M-s M-l" #'consult-line
"M-s M-m" #'consult-mark
"M-s M-y" #'consult-yank-pop
"M-s M-s" #'consult-outline)
(add-to-list 'consult-mode-histories '(vc-git-log-edit-mode . log-edit-comment-ring))
(setq consult-line-numbers-widen t)
;; (setq completion-in-region-function #'consult-completion-in-region)
(setq consult-async-min-input 3)
(setq consult-async-input-debounce 0.5)
(setq consult-async-input-throttle 0.8)
(setq consult-narrow-key nil)
(setq consult-find-args
(concat "find . -not ( "
"-path */.git* -prune "
"-or -path */.cache* -prune )"))
(setq consult-preview-key 'any)
(setq consult-project-function nil) ; always work from the current directory (use `cd' to switch directory)
(add-hook 'completion-list-mode-hook #'consult-preview-at-point-mode)
(require 'consult-imenu) ; the `imenu' extension is in its own file
(prot-emacs-keybind global-map
"M-g M-g" #'consult-goto-line
"M-K" #'consult-keep-lines ; M-S-k is similar to M-S-5 (M-%)
"M-F" #'consult-focus-lines ; same principle
"M-s M-b" #'consult-buffer
"M-s M-f" #'consult-find
"M-s M-g" #'consult-grep
"M-s M-h" #'consult-history
"M-s M-i" #'consult-imenu
"M-s M-l" #'consult-line
"M-s M-m" #'consult-mark
"M-s M-y" #'consult-yank-pop
"M-s M-s" #'consult-outline)
(define-key consult-narrow-map (kbd "?") #'consult-narrow-help)
(define-key minibuffer-local-map (kbd "C-s") #'consult-history)
(with-eval-after-load 'pulsar
;; see my `pulsar' package: <https://protesilaos.com/emacs/pulsar>
(setq consult-after-jump-hook nil) ; reset it to avoid conflicts with my function
(prot-emacs-hook
consult-after-jump-hook
(pulsar-recenter-top pulsar-reveal-entry)
nil
pulsar)))
(dolist (fn '(pulsar-recenter-top pulsar-reveal-entry))
(add-hook 'consult-after-jump-hook fn))))
;;; Extended minibuffer actions and more (embark.el)
(when prot-emacs-completion-extras
(prot-emacs-configure
(prot-emacs-install embark)
;;; Extended minibuffer actions and more (embark.el and prot-embark.el)
(prot-emacs-package embark
(:install t)
(:delay 5)
(setq prefix-help-command #'embark-prefix-help-command)
;; (setq prefix-help-command #'describe-prefix-bindings) ; the default of the above
(add-hook 'embark-collect-mode-hook #'prot-common-truncate-lines-silently)
(setq embark-confirm-act-all nil)
(setq embark-mixed-indicator-both nil)
(setq embark-mixed-indicator-delay 1.0)
(setq embark-indicators '(embark-mixed-indicator embark-highlight-indicator))
(setq embark-verbose-indicator-nested nil) ; I think I don't have them, but I do not want them either
(setq embark-verbose-indicator-buffer-sections '(bindings))
(setq embark-verbose-indicator-excluded-actions
'(embark-cycle embark-act-all embark-collect embark-export embark-insert))
(prot-emacs-keybind minibuffer-local-map
"C-c C-c" #'embark-collect
"C-c C-e" #'embark-export)
;; I never cycle and want to disable the key. Normally, a nil value
;; disables a key binding but here that value is interpreted as the
;; binding for `embark-act'. So I just add some obscure key that I
;; do not have. I absolutely do not want to cycle by accident!
(setq embark-cycle-key "<XF86Travel>")
;; Needed for correct exporting while using Embark with Consult commands.
(prot-emacs-install embark-consult)
;; The minimal indicator shows cycling options, but I have no use
;; for those. I want it to be silent.
(defun prot/embark-no-minimal-indicator ())
(advice-add #'embark-minimal-indicator :override #'prot/embark-no-minimal-indicator)
(with-eval-after-load 'consult
(require 'embark-consult))))
(defun prot/embark-act-no-quit ()
"Call `embark-act' but do not quit after the action."
(interactive)
(let ((embark-quit-after-action nil))
(call-interactively #'embark-act)))
(defun prot/embark-act-quit ()
"Call `embark-act' and quit after the action."
(interactive)
(let ((embark-quit-after-action t))
(call-interactively #'embark-act))
(when (and (> (minibuffer-depth) 0)
(derived-mode-p 'completion-list-mode))
(abort-recursive-edit)))
(dolist (map (list global-map embark-collect-mode-map minibuffer-local-filename-completion-map))
(define-key map (kbd "C-,") #'prot/embark-act-no-quit)
(define-key map (kbd "C-.") #'prot/embark-act-quit))
;; I do not want `embark-org' and am not sure what is loading it.
;; So I just unsert all the keymaps...
(with-eval-after-load 'embark-org
(defvar prot/embark-org-keymaps
'(embark-org-table-cell-map
embark-org-table-map
embark-org-link-copy-map
embark-org-link-map
embark-org-src-block-map
embark-org-item-map
embark-org-plain-list-map
embark-org-export-in-place-map)
"List of Embark keymaps for Org.")
;; Reset `prot/embark-org-keymaps'.
(seq-do
(lambda (keymap)
(set keymap (make-sparse-keymap)))
prot/embark-org-keymaps)))
(prot-emacs-package prot-embark
(:delay 5)
(setq embark-keymap-alist
'((buffer prot-embark-buffer-map)
(command prot-embark-command-map)
(expression prot-embark-expression-map)
(file prot-embark-file-map)
(function prot-embark-function-map)
(identifier prot-embark-identifier-map)
(package prot-embark-package-map)
(region prot-embark-region-map)
(symbol prot-embark-symbol-map)
(url prot-embark-url-map)
(variable prot-embark-variable-map))))
;; Needed for correct exporting while using Embark with Consult
;; commands.
(prot-emacs-package embark-consult (:install t) (:delay 5))
;;; Detailed completion annotations (marginalia.el)
(when prot-emacs-completion-extras
(prot-emacs-configure
(prot-emacs-install marginalia)
(setq marginalia-max-relative-age 0) ; absolute time
(marginalia-mode 1)))
(prot-emacs-package marginalia
(:install t)
(:delay 5)
(setq marginalia-max-relative-age 0) ; absolute time
(marginalia-mode 1))
;;;; Custom completion annotations
(prot-emacs-package prot-marginalia
(:delay 5)
(setq marginalia-annotator-registry
'((bookmark prot-marginalia-bookmark)
(buffer prot-marginalia-buffer)
(command marginalia-annotate-command)
(function prot-marginalia-symbol)
(symbol prot-marginalia-symbol)
(variable prot-marginalia-symbol)
(face marginalia-annotate-face)
(imenu marginalia-annotate-imenu)
(package prot-marginalia-package)
(unicode-name marginalia-annotate-char))))
;;; The minibuffer user interface (mct, vertico, or none)
(when prot-emacs-completion-ui

View file

@ -1,150 +1,136 @@
;;; Dired file manager and prot-dired.el extras
(prot-emacs-configure
(prot-emacs-package dired
(:delay 2)
(setq dired-recursive-copies 'always)
(setq dired-recursive-deletes 'always)
(setq delete-by-moving-to-trash t))
(setq delete-by-moving-to-trash t)
(setq dired-listing-switches
"-AGFhlv --group-directories-first --time-style=long-iso")
(setq dired-dwim-target t)
(setq dired-auto-revert-buffer #'dired-directory-changed-p) ; also see `dired-do-revert-buffer'
(setq dired-make-directory-clickable t) ; Emacs 29.1
(setq dired-free-space nil) ; Emacs 29.1
(setq dired-mouse-drag-files t) ; Emacs 29.1
(setq dired-guess-shell-alist-user ; those are the suggestions for ! and & in Dired
'(("\\.\\(png\\|jpe?g\\|tiff\\)" "feh" "xdg-open")
("\\.\\(mp[34]\\|m4a\\|ogg\\|flac\\|webm\\|mkv\\)" "mpv" "xdg-open")
(".*" "xdg-open")))
(setq dired-listing-switches "-AGFhlv --group-directories-first --time-style=long-iso")
(add-hook 'dired-mode-hook #'dired-hide-details-mode)
(add-hook 'dired-mode-hook #'hl-line-mode)
(setq dired-dwim-target t)
;; In Emacs 29 there is a binding for `repeat-mode' which let you
;; repeat C-x C-j just by following it up with j. For me, this is a
;; problem as j calls `dired-goto-file', which I often use.
(define-key dired-jump-map (kbd "j") nil))
(setq dired-guess-shell-alist-user ; those are the suggestions for ! and & in Dired
'(("\\.\\(png\\|jpe?g\\|tiff\\)" "feh *" "feh" "xdg-open")
("\\.\\(mp[34]\\|m4a\\|ogg\\|flac\\|webm\\|mkv\\)" "mpv *" "mpv" "xdg-open")
(".*" "xdg-open")))
(prot-emacs-package dired-aux
(:delay 2)
(setq dired-isearch-filenames 'dwim)
(setq dired-create-destination-dirs 'ask) ; Emacs 27
(setq dired-vc-rename-file t) ; Emacs 27
(setq dired-do-revert-buffer (lambda (dir) (not (file-remote-p dir)))) ; Emacs 28
(setq dired-create-destination-dirs-on-trailing-dirsep t) ; Emacs 29
(prot-emacs-configure
(with-eval-after-load 'dired
(setq dired-auto-revert-buffer #'dired-directory-changed-p) ; also see `dired-do-revert-buffer'
(setq dired-make-directory-clickable t) ; Emacs 29.1
(setq dired-free-space nil) ; Emacs 29.1
(setq dired-mouse-drag-files t) ; Emacs 29.1
(prot-emacs-keybind dired-mode-map
"C-+" #'dired-create-empty-file
"M-s f" nil
"C-<return>" #'dired-do-open ; Emacs 30
"C-x v v" #'dired-vc-next-action)) ; Emacs 28
(prot-emacs-hook dired-mode-hook (dired-hide-details-mode hl-line-mode))
;; ;; NOTE 2021-05-10: I do not use `find-dired' and related commands
;; ;; because there are other tools that offer a better interface, such
;; ;; as `consult-find', `consult-grep', `project-find-file',
;; ;; `project-find-regexp', `prot-vc-git-grep'.
;; (prot-emacs-package find-dired
;; (setq find-ls-option
;; '("-ls" . "-AGFhlv --group-directories-first --time-style=long-iso"))
;; (setq find-name-arg "-iname"))
;; In Emacs 29 there is a binding for `repeat-mode' which lets you
;; repeat C-x C-j just by following it up with j. For me, this is a
;; problem as j calls `dired-goto-file', which I often use.
(define-key dired-jump-map (kbd "j") nil)))
(prot-emacs-package dired-x
(:delay 2)
(setq dired-clean-up-buffers-too t)
(setq dired-clean-confirm-killing-deleted-buffers t)
(setq dired-x-hands-off-my-keys t) ; easier to show the keys I use
(setq dired-bind-man nil)
(setq dired-bind-info nil)
(define-key dired-mode-map (kbd "I") #'dired-info))
(prot-emacs-configure
(with-eval-after-load 'dired
(require 'dired-aux)
(prot-emacs-package prot-dired
(:delay 2)
(add-hook 'dired-mode-hook #'prot-dired-setup-imenu)
(prot-emacs-keybind dired-mode-map
"C-+" #'dired-create-empty-file
"M-s f" #'nil
"C-<return>" #'dired-do-open ; Emacs 30
"C-x v v" #'dired-vc-next-action) ; Emacs 28
(prot-emacs-keybind dired-mode-map
"i" #'prot-dired-insert-subdir ; override `dired-maybe-insert-subdir'
"/" #'prot-dired-limit-regexp
"C-c C-l" #'prot-dired-limit-regexp
"M-n" #'prot-dired-subdirectory-next
"C-c C-n" #'prot-dired-subdirectory-next
"M-p" #'prot-dired-subdirectory-previous
"C-c C-p" #'prot-dired-subdirectory-previous
"M-s G" #'prot-dired-grep-marked-files)) ; M-s g is `prot-search-grep'
(setq dired-isearch-filenames 'dwim)
(setq dired-create-destination-dirs 'ask) ; Emacs 27
(setq dired-vc-rename-file t) ; Emacs 27
(setq dired-do-revert-buffer (lambda (dir) (not (file-remote-p dir)))) ; Emacs 28
(setq dired-create-destination-dirs-on-trailing-dirsep t) ; Emacs 29
(prot-emacs-package dired-subtree
(:install t)
(:delay 2)
(setq dired-subtree-use-backgrounds nil)
(prot-emacs-keybind dired-mode-map
"<tab>" #'dired-subtree-toggle
"<backtab>" #'dired-subtree-remove)) ; S-TAB
(require 'dired-x)
(define-key dired-mode-map (kbd "I") #'dired-info)
(setq dired-clean-up-buffers-too t)
(setq dired-clean-confirm-killing-deleted-buffers t)
(setq dired-x-hands-off-my-keys t) ; easier to show the keys I use
(setq dired-bind-man nil)
(setq dired-bind-info nil)))
(prot-emacs-configure
(with-eval-after-load 'dired
(require 'prot-dired)
(add-hook 'dired-mode #'prot-dired-setup-imenu)
(prot-emacs-keybind dired-mode-map
"f" nil ; disable `dired-find-file' as it is on RET already
"i" #'prot-dired-insert-subdir ; override `dired-maybe-insert-subdir'
"/" #'prot-dired-limit-regexp
"C-M-<return>" #'prot-dired-open-directory-externally ; C-<return> is `dired-do-open'
"C-c C-l" #'prot-dired-limit-regexp
"M-n" #'prot-dired-subdirectory-next
"C-c C-s" #'prot-dired-search-flat-list
"C-c C-r" #'prot-dired-search-flat-list-since-days
"C-c C-n" #'prot-dired-subdirectory-next
"C-c C-p" #'prot-dired-subdirectory-previous
"M-s G" #'prot-dired-grep-marked-files ; M-s g is `prot-search-grep'
"M-p" #'prot-dired-subdirectory-previous)))
(prot-emacs-configure
(prot-emacs-install dired-subtree)
(with-eval-after-load 'dired
(prot-emacs-keybind dired-mode-map
"<tab>" #'dired-subtree-toggle
"TAB" #'dired-subtree-toggle
"<backtab>" #'dired-subtree-remove
"S-TAB" #'dired-subtree-remove)
(setq dired-subtree-use-backgrounds nil)))
(prot-emacs-configure
(prot-emacs-package wdired
(:delay 2)
(setq wdired-allow-to-change-permissions t)
(setq wdired-create-parent-directories t))
(prot-emacs-configure
(with-eval-after-load 'image-dired
(define-key image-dired-thumbnail-mode-map (kbd "<return>") #'image-dired-thumbnail-display-external)
(setq image-dired-thumbnail-storage 'standard)
(setq image-dired-external-viewer "xdg-open")
(setq image-dired-thumb-size 80)
(setq image-dired-thumb-margin 2)
(setq image-dired-thumb-relief 0)
(setq image-dired-thumbs-per-row 4)))
;;; Automatically preview Dired file at point (dired-preview.el)
;; One of my packages: <https://protesilaos.com/emacs>
(prot-emacs-configure
(prot-emacs-install dired-preview)
(with-eval-after-load 'dired
(require 'dired-preview)
(add-hook 'dired-mode-hook (lambda ()
(when (string-match-p "Pictures" default-directory)
(dired-preview-mode 1))))
(define-key dired-mode-map (kbd "V") #'dired-preview-mode)
(setq dired-preview-trigger-on-start nil)
(setq dired-preview-max-size (* (expt 2 20) 10))
(setq dired-preview-delay 0.5)
(setq dired-preview-ignored-extensions-regexp
(concat "\\."
"\\(gz\\|"
"zst\\|"
"tar\\|"
"xz\\|"
"rar\\|"
"zip\\|"
"iso\\|"
"epub"
"\\)"))
(setq dired-preview-display-action-alist #'dired-preview-display-action-alist-below)))
(prot-emacs-package image-dired
(:delay 60)
(setq image-dired-thumbnail-storage 'standard)
(setq image-dired-external-viewer "xdg-open")
(setq image-dired-thumb-size 80)
(setq image-dired-thumb-margin 2)
(setq image-dired-thumb-relief 0)
(setq image-dired-thumbs-per-row 4)
(define-key image-dired-thumbnail-mode-map
(kbd "<return>") #'image-dired-thumbnail-display-external))
;;; dired-like mode for the trash (trashed.el)
(prot-emacs-configure
(prot-emacs-install trashed)
(prot-emacs-package trashed
(:install t)
(:delay 60)
(setq trashed-action-confirmer 'y-or-n-p)
(setq trashed-use-header-line t)
(setq trashed-sort-key '("Date deleted" . t))
(setq trashed-date-format "%Y-%m-%d %H:%M:%S"))
;;; Automatically preview Dired file at point (dired-preview.el)
;; One of my packages: <https://protesilaos.com/emacs>
(prot-emacs-package dired-preview
(:install t)
(:delay 60)
;; These are all set to their default values. I keep them here for
;; reference.
(setq dired-preview-max-size (* (expt 2 20) 6))
(setq dired-preview-delay 0.3)
(setq dired-preview-ignored-extensions-regexp
(concat "\\."
"\\(mkv\\|" "webm\\|" "mp4\\|" "mp3\\|" "ogg\\|" "m4a\\|"
"gz\\|" "zst\\|" "tar\\|" "xz\\|" "rar\\|" "zip\\|"
"iso\\|" "epub\\|" "\\)"))
(add-hook 'dired-mode-hook
(lambda ()
(when (string-match-p "Pictures" default-directory)
(dired-preview-mode 1)))))
;;; Play back media with Dired (mandoura.el)
;; This is yet another package of mine: <https://protesilaos.com/emacs>
(prot-emacs-configure
(prot-emacs-install-vc
mandoura
"https://github.com/protesilaos/mandoura.git")
(with-eval-after-load 'dired
(require 'mandoura)
(prot-emacs-keybind global-map
"M-<AudioPlay>" #'mandoura-return-track-title-and-time
"M-<XF86AudioPlay>" #'mandoura-return-track-title-and-time)
(prot-emacs-keybind dired-mode-map
"M-<return>" #'mandoura-play-playlist
"M-RET" #'mandoura-play-playlist)
(setq mandoura-saved-playlist-directory "~/Music/playlists/")))
(prot-emacs-package mandoura
(:install "https://git.sr.ht/~protesilaos/mandoura")
(:delay 5)
(setq mandoura-saved-playlist-directory "~/Music/playlists/")
(define-key dired-mode-map (kbd "M-<return>") #'mandoura-play-files)
(define-key global-map (kbd "M-<AudioPlay>") #'mandoura-return-track-title-and-time)
(define-key global-map (kbd "M-<XF86AudioPlay>") #'mandoura-return-track-title-and-time))
(provide 'prot-emacs-dired)

View file

@ -1,32 +0,0 @@
;;; The Doric themes
(prot-emacs-configure
(prot-emacs-install doric-themes)
(prot-emacs-keybind global-map
"<f5>" #'doric-themes-rotate
"C-<f5>" #'doric-themes-select)
(doric-themes-load-random
(if (prot-emacs-gnome-prefers-dark-p)
'dark
'light)))
;; For testing purposes
(prot-emacs-comment
(:eval nil)
(add-to-list 'load-path "~/Git/Projects/doric-themes/")
(require 'doric-themes)
(prot-emacs-keybind global-map
"<f5>" #'doric-themes-rotate
"C-<f5>" #'doric-themes-select)
(doric-themes-load-random
(if (prot-emacs-gnome-prefers-dark-p)
'dark
'light)))
(provide 'prot-emacs-doric-themes)

View file

@ -1,21 +1,13 @@
;;; The Ef (εὖ) themes
(prot-emacs-configure
(prot-emacs-install ef-themes)
(ef-themes-take-over-modus-themes-mode 1)
(prot-emacs-keybind global-map
"<f5>" #'modus-themes-rotate
"C-<f5>" #'modus-themes-select
"M-<f5>" #'modus-themes-load-random)
(setq modus-themes-variable-pitch-ui t
modus-themes-mixed-fonts t
modus-themes-bold-constructs t
modus-themes-italic-constructs t
modus-themes-to-rotate nil ; defaults to the return value of `modus-themes-get-themes'
modus-themes-headings ; read the manual's entry of the doc string
;; The themes are customisable. Read the manual:
;; <https://protesilaos.com/emacs/ef-themes>.
(prot-emacs-package ef-themes
(:install t)
(:delay 1)
(setq ef-themes-variable-pitch-ui t
ef-themes-mixed-fonts t
ef-themes-headings ; read the manual's entry of the doc string
'((0 . (variable-pitch light 1.9))
(1 . (variable-pitch light 1.8))
(2 . (variable-pitch regular 1.7))
@ -28,28 +20,78 @@
(agenda-structure . (variable-pitch light 1.9))
(t . (variable-pitch 1.1))))
(if (prot-emacs-gnome-prefers-dark-p)
(modus-themes-load-random 'dark)
(modus-themes-load-random 'light)))
;; The `ef-themes' provide lots of themes. I want to pick one at
;; random when I start Emacs: the `ef-themes-load-random' does just
;; that (it can be called interactively as well). I just check with
;; my desktop environment to determine if the choice should be about
;; a light or a dark theme. Those functions are in my init.el.
(if (prot-emacs-theme-environment-dark-p)
(ef-themes-load-random 'dark)
(ef-themes-load-random 'light))
(prot-emacs-comment
(:eval nil)
(define-key global-map (kbd "<f5>") #'ef-themes-select))
(add-to-list 'load-path "~/Git/Projects/modus-themes/")
(add-to-list 'load-path "~/Git/Projects/ef-themes/")
;; ;; NOTE: For testing purposes
;; (progn
;; (mapc #'disable-theme custom-enabled-themes)
;;
;; (add-to-list 'load-path "/home/prot/Git/Projects/ef-themes/")
;;
;; (require 'ef-themes)
;; (load-theme 'ef-autumn t t)
;; (load-theme 'ef-bio t t)
;; (load-theme 'ef-cherie t t)
;; (load-theme 'ef-chloe-dark t t)
;; (load-theme 'ef-chloe-light t t)
;; (load-theme 'ef-cyprus t t)
;; (load-theme 'ef-dark t t)
;; (load-theme 'ef-day t t)
;; (load-theme 'ef-deuteranopia-dark t t)
;; (load-theme 'ef-deuteranopia-light t t)
;; (load-theme 'ef-duo-dark t t)
;; (load-theme 'ef-duo-light t t)
;; (load-theme 'ef-frost t t)
;; (load-theme 'ef-kassio t t)
;; (load-theme 'ef-light t t)
;; (load-theme 'ef-melissa-dark t t)
;; (load-theme 'ef-melissa-light t t)
;; (load-theme 'ef-night t t)
;; (load-theme 'ef-spring t t)
;; (load-theme 'ef-summer t t)
;; (load-theme 'ef-symbiosis t t)
;; (load-theme 'ef-trio-dark t t)
;; (load-theme 'ef-trio-light t t)
;; (load-theme 'ef-tritanopia-dark t t)
;; (load-theme 'ef-tritanopia-light t t)
;; (load-theme 'ef-winter t t)
;;
;; (setq ef-themes-headings ; read the manual's entry or the doc string
;; '((0 . (variable-pitch light 1.9))
;; (1 . (variable-pitch light 1.8))
;; (2 . (variable-pitch light 1.7))
;; (3 . (variable-pitch semilight 1.6))
;; (4 . (variable-pitch semilight 1.5))
;; (5 . (variable-pitch regular 1.4))
;; (6 . (variable-pitch regular 1.3))
;; (7 . (variable-pitch 1.2)) ; absence of weight means `bold'
;; (agenda-date . (semilight 1.5))
;; (agenda-structure . (variable-pitch light 1.9))
;; (t . (variable-pitch 1.1))))
;;
;; ;; They are nil by default...
;; (setq ef-themes-mixed-fonts t
;; ef-themes-variable-pitch-ui nil)
;;
;; (mapcar (lambda (theme)
;; (add-to-list
;; 'custom-theme-load-path
;; (concat "/home/prot/Git/Projects/ef-themes/" (symbol-name theme) "-theme.el")))
;; (ef-themes--list-enabled-themes))
;;
;; (if (prot-emacs-theme-environment-dark-p)
;; (ef-themes-load-random 'dark)
;; (ef-themes-load-random 'light))
;;
;; (define-key global-map (kbd "<f5>") #'ef-themes-select))
(require 'modus-themes)
(require 'ef-themes)
(ef-themes-take-over-modus-themes-mode 1)
(prot-emacs-keybind global-map
"<f5>" #'modus-themes-rotate
"C-<f5>" #'modus-themes-select
"M-<f5>" #'modus-themes-load-random)
(if (prot-emacs-gnome-prefers-dark-p)
(modus-themes-load-random 'dark)
(modus-themes-load-random 'light)))
(provide 'prot-emacs-ef-themes)
(provide 'prot-emacs-ef-themes)

View file

@ -1,69 +1,52 @@
;;; Client-agnostic email settings
(prot-emacs-configure
(:delay 1)
;;;; File with authentication credentials (`auth-source')
(prot-emacs-configure
(setq auth-sources '("~/.authinfo.gpg")
user-full-name "Protesilaos"
user-mail-address "prot@protesilaos.com"))
user-full-name "Protesilaos Stavrou"
user-mail-address "public@protesilaos.com")
;;;; Encryption settings (`mm-encode' and `mml-sec')
(prot-emacs-configure
(setq mm-encrypt-option nil ; use 'guided for both if you need more control
mm-sign-option nil)
;;;; Encoding settings (`mm-encode')
(setq mm-encrypt-option nil ; use 'guided if you need more control
mm-sign-option nil) ; same
;;;; Encryption settings (`mml-sec')
(setq mml-secure-openpgp-encrypt-to-self t
mml-secure-openpgp-sign-with-sender t
mml-secure-smime-encrypt-to-self t
mml-secure-smime-sign-with-sender t)
;;;; Message composition (`message')
(setq mail-user-agent 'message-user-agent
mail-header-separator (purecopy "*****")
message-elide-ellipsis "\n> [... %l lines elided]\n"
compose-mail-user-agent-warnings nil
message-mail-user-agent t ; use `mail-user-agent'
mail-signature "Protesilaos Stavrou\nhttps://protesilaos.com\n"
message-signature "Protesilaos Stavrou\nhttps://protesilaos.com\n"
message-citation-line-function #'message-insert-formatted-citation-line
message-citation-line-format (concat "> From: %f\n"
"> Date: %a, %e %b %Y %T %z\n"
">")
message-ignored-cited-headers "" ; default is "." for all headers
message-confirm-send nil
message-kill-buffer-on-exit t
message-wide-reply-confirm-recipients t)
;; (add-to-list 'mm-body-charset-encoding-alist '(utf-8 . base64))
(add-hook 'message-setup-hook #'message-sort-headers)
(setq mail-user-agent 'message-user-agent
message-mail-user-agent t) ; use `mail-user-agent'
(setq mail-header-separator "--text follows this line--")
(setq message-elide-ellipsis "\n> [... %l lines elided]\n")
(setq compose-mail-user-agent-warnings nil)
(setq message-signature "Protesilaos\nhttps://protesilaos.com\n"
mail-signature message-signature)
(setq message-citation-line-function #'message-insert-formatted-citation-line)
(setq message-citation-line-format (concat "> From: %f\n"
"> Date: %a, %e %b %Y %T %z\n"
">")
message-ignored-cited-headers "") ; default is "." for all headers
(setq message-confirm-send nil)
(setq message-kill-buffer-on-exit t)
;; (add-to-list 'mm-body-charset-encoding-alist '(utf-8 . base64))
(setq message-forward-as-mime t)
(setq message-wide-reply-confirm-recipients nil)
(defun prot/message-copy-body ()
"Copy the body of the current message buffer."
(interactive)
(save-excursion
(let ((beginning (message-goto-body))
(end (progn
(message-goto-signature)
(forward-line -2)
(point))))
(kill-new (buffer-substring-no-properties beginning end))
(message "Copied the body of the current message buffer")
(setq this-command 'kill-ring-save))))
(with-eval-after-load 'message
;; Override `message-insert-signature'.
(define-key message-mode-map (kbd "C-c C-w") #'prot/message-copy-body)))
;;;; Add attachments from Dired (`gnus-dired' does not require `gnus')
(prot-emacs-configure
(add-hook 'dired-mode-hook #'turn-on-gnus-dired-mode))
(add-hook 'dired-mode-hook #'turn-on-gnus-dired-mode)
;;;; `sendmail' (mail transfer agent)
(prot-emacs-configure
(with-eval-after-load 'message
(setq send-mail-function #'smtpmail-send-it)
(setq smtpmail-smtp-server "mail.gandi.net")
(setq smtpmail-smtp-service 587)
(setq smtpmail-stream-type 'starttls)))
(setq send-mail-function 'sendmail-send-it
;; ;; NOTE 2023-08-08: We do not need this if we have the Arch
;; ;; Linux `msmtp-mta' package installed: it replaces the
;; ;; generic sendmail executable with msmtp.
;;
;; sendmail-program (executable-find "msmtp")
message-sendmail-envelope-from 'header))
(when (executable-find "notmuch")
(require 'prot-emacs-notmuch))

View file

@ -1,150 +1,100 @@
;;; Essential configurations
(prot-emacs-configure
(:delay 1)
;; NOTE 2023-05-20: Normally those would not have to be `require'd
;; as every point of entry is autoloaded. But Emacs does not have
;; an autoloads file for them, as they are not installed the usual
;; way and I do not want to generate such a file: the `require' is
;; fine.
(require 'prot-common)
(require 'prot-simple)
(require 'prot-scratch)
(require 'prot-pair)
(require 'prot-comment)
(require 'prot-prefix)
;;;; General settings and common custom functions (prot-simple.el)
(setq backward-delete-char-untabify-method nil)
(setq read-minibuffer-restore-windows nil) ; Emacs 28
(setq blink-matching-paren nil)
(setq custom-unlispify-tag-names nil)
(setq delete-pair-blink-delay 0.1) ; Emacs28 -- see `prot-simple-delete-pair-dwim'
(setq delete-pair-push-mark t) ; Emacs 31
(setq echo-keystrokes-help nil) ; Emacs 30
(setq epa-keys-select-method 'minibuffer) ; Emacs 30
(setq eval-expression-print-length nil)
(setq find-library-include-other-files nil) ; Emacs 29
(setq help-window-select t)
(setq help-window-keep-selected t) ; Emacs 29
(setq kill-do-not-save-duplicates t)
(setq mode-require-final-newline 'visit-save)
(setq next-error-recenter '(4)) ; center of the window
(setq remote-file-name-inhibit-auto-save t) ; Emacs 30
(setq find-library-include-other-files nil) ; Emacs 29
(setq remote-file-name-inhibit-delete-by-moving-to-trash t) ; Emacs 30
(setq save-interprogram-paste-before-kill t)
(setq remote-file-name-inhibit-auto-save t) ; Emacs 30
(setq tramp-connection-timeout (* 60 10)) ; seconds
(setq trusted-content '("~/Git/Projects/" "~/Coach/")) ; Emacs 30
(setq truncate-partial-width-windows nil)
;; Keys I unbind here are either to avoid accidents or to bind them
(prot-emacs-keybind global-map
"<f2>" #'toggle-input-method ; F2 overrides that two-column gimmick. Sorry, but no!
"<insert>" nil
"<menu>" nil
"C-M-w" nil ; never use it
"C-x C-d" nil ; never use it
"C-x C-v" nil ; never use it
"C-z" nil ; I have a window manager, thanks!
"C-x C-z" nil ; same idea as above
"C-x C-c" nil ; avoid accidentally exiting Emacs
"C-x C-c C-c" #'save-buffers-kill-emacs ; more cumbersome, less error-prone
"C-h h" nil ; Never show that "hello" file
"M-`" nil
"M-o" #'delete-blank-lines ; alias for C-x C-o
"M-SPC" #'cycle-spacing
"M-z" #'zap-up-to-char ; NOT `zap-to-char'
"M-c" #'capitalize-dwim
"M-l" #'downcase-dwim ; "lower" case
"M-u" #'upcase-dwim
"M-=" #'count-words
"C-x O" #'next-multiframe-window
"C-h K" #'describe-keymap ; overrides `Info-goto-emacs-key-command-node'
"C-h u" #'apropos-user-option
"C-h F" #'apropos-function ; lower case is `describe-function'
"C-h V" #'apropos-variable ; lower case is `describe-variable'
"C-h L" #'apropos-library ; lower case is `view-lossage'
"C-h c" #'describe-char) ; overrides `describe-key-briefly'
(prot-emacs-keybind prog-mode-map
"C-M-d" #'up-list ; confusing name for what looks like "down" to me
"<C-M-backspace>" #'backward-kill-sexp)
;; Keymap for buffers (Emacs28)
(prot-emacs-keybind ctl-x-x-map
"f" #'follow-mode ; override `font-lock-update'
"r" #'rename-uniquely
"l" #'visual-line-mode))
(prot-emacs-configure
(require 'prot-common)
(defvar prot/fundamental-mode-hook nil
"Normal hook for `fundamental-mode' (which is missing by default).")
(defun prot/fundamental-mode-run-hook (&rest args)
"Apply ARGS and then run `prot/fundamental-mode-hook'."
(apply args)
(run-hooks 'prot/fundamental-mode-hook))
(advice-add #'fundamental-mode :around #'prot/fundamental-mode-run-hook)
(prot-emacs-hook
(text-mode-hook prog-mode-hook dired-mode-hook prot/fundamental-mode-hook hexl-mode-hook comint-mode-hook)
prot-common-truncate-lines-silently)
;; NEVER tell me which key can call a command that I specifically
;; invoked with M-x: I have a good reason to use it that way.
(advice-add #'execute-extended-command--describe-binding-msg :override #'prot-common-ignore))
(prot-emacs-configure
(require 'prot-simple)
(setq save-interprogram-paste-before-kill t)
(setq mode-require-final-newline 'visit-save)
(setq-default truncate-partial-width-windows nil)
(setq eval-expression-print-length nil)
(setq kill-do-not-save-duplicates t)
(setq duplicate-line-final-position -1 ; both are Emacs 29
duplicate-region-final-position -1)
(setq scroll-error-top-bottom t)
(setq prot-simple-date-specifier "%F")
(setq prot-simple-time-specifier "%R %z")
(advice-add #'save-buffers-kill-emacs :before #'prot-simple-display-unsaved-buffers-on-exit)
(setq prot-scratch-default-mode 'text-mode)
;; All `prot-simple-override-mode' does is activate a keymap.
;; Below I add keys to that map. Because the mode is enabled
;; globally, those keys take precedence over the ones specified by
;; any given major mode. In principle, this means that my keys will
;; always work (though technically they can be overriden by another
;; minor mode, depending on which one is evaluated last).
(prot-simple-override-mode 1)
;;;; Comments (prot-comment.el)
(setq comment-empty-lines t)
(setq comment-fill-column nil)
(setq comment-multi-line t)
(setq comment-style 'multi-line)
(setq-default comment-column 0)
(defun prot/simple-hex-highlight ()
(when-let* ((file buffer-file-name)
((derived-mode-p 'emacs-lisp-mode))
((string-match-p "-theme" file)))
(prot-simple-hex-color-mode 1)))
(add-hook 'emacs-lisp-mode-hook #'prot/simple-hex-highlight)
(define-key ctl-x-x-map (kbd "c") #'prot-simple-hex-color-mode) ; C-x x c
(prot-emacs-keybind prot-simple-override-mode-map
"C-a" #'prot-simple-duplicate-line-or-region ; "again" mnemonic, overrides `move-beginning-of-line'
"C-d" #'prot-simple-delete-line ; overrides `delete-char'
"C-v" #'prot-simple-multi-line-below ; overrides `scroll-up-command'
"<next>" #'prot-simple-multi-line-below ; overrides `scroll-up-command'
"M-v" #'prot-simple-multi-line-above ; overrides `scroll-down-command'
"<prior>" #'prot-simple-multi-line-above ; overrides `scroll-down-command'
"C-M-i" #'prot-simple-indent-dwim ; overrides `completion-at-point'
"C-M-\\" #'prot-simple-indent-dwim ; overrides `indent-region'
"C-M-c" #'completion-at-point) ; overrides `exit-recursive-edit'
(setq prot-comment-comment-keywords '("TODO" "NOTE" "XXX" "REVIEW" "FIXME"))
(setq prot-comment-timestamp-format-concise "%F")
(setq prot-comment-timestamp-format-verbose "%F %T %z")
;; General commands
(prot-emacs-keybind global-map
"C-h h" #'prot-simple-describe-at-point
"<escape>" #'prot-simple-keyboard-quit-dwim
"<insert>" nil
"C-x C-z" nil
"C-x C-c" nil ; avoid accidentally exiting Emacs
"C-x C-c C-c" #'save-buffers-kill-emacs
"C-h h" nil
"M-`" nil
"C-z" prot-prefix-map
"<f2>" prot-prefix-map ; override that two-column gimmick
"C-g" #'prot-simple-keyboard-quit-dwim
"C-M-SPC" #'prot-simple-mark-sexp
"C-x 0" #'prot-simple-delete-window-dwim ; overrides `delete-window'
"C-x ." #'prot-simple-goto-definition ; overrides `set-fill-prefix'
"C-h ." #'prot-simple-describe-symbol ; overrides `display-local-help'
"C-h F" #'describe-face ; overrides `Info-goto-emacs-command-node'
"C-h K" #'describe-keymap ; overrides `Info-goto-emacs-key-command-node'
"C-h c" #'describe-char ; overrides `describe-key-briefly'
"C-M-SPC" #'prot-simple-mark-sexp ; will be overriden by `expreg' if tree-sitter is available
;; ;; NOTE 2023-12-17: I am not happy with these. Will rewrite them.
;; "C-c +" #'prot-simple-number-increment
;; "C-c -" #'prot-simple-number-decrement
;; Commands for lines
"C-S-d" #'prot-simple-delete-line-backward
"C-S-k" #'prot-simple-kill-line-backward
"M-k" #'prot-simple-copy-line-forward
"M-K" #'prot-simple-copy-line-backward
"M-j" #'delete-indentation
"C-w" #'prot-simple-kill-region
"M-w" #'prot-simple-kill-ring-save
"M-o" #'delete-blank-lines ; alias for C-x C-o
"M-k" #'prot-simple-kill-line-backward
"C-S-w" #'prot-simple-copy-line
"C-S-d" #'duplicate-dwim ; Emacs 29
"C-S-y" #'prot-simple-yank-replace-line-or-region
"M-SPC" #'cycle-spacing
"C-v" #'prot-simple-multi-line-below ; overrides `scroll-up-command'
"M-v" #'prot-simple-multi-line-above ; overrides `scroll-down-command'
"<C-return>" #'prot-simple-new-line-below
"<C-S-return>" #'prot-simple-new-line-above
"C-x x a" #'prot-simple-auto-fill-visual-line-mode ; auto-fill/visual-line toggle
;; Commands for text insertion or manipulation
"C-=" #'prot-simple-insert-date
"C-<" #'prot-simple-escape-url-dwim
"C->" #'prot-simple-escape-url-dwim
;; "C->" #'prot-simple-insert-line-prefix-dwim
"C-'" #'prot-pair-insert
"M-'" #'prot-pair-insert
"M-\\" #'prot-pair-delete
"M-z" #'zap-up-to-char ; NOT `zap-to-char'
"M-Z" #'prot-simple-zap-to-char-backward
"<C-M-backspace>" #'backward-kill-sexp
"M-c" #'capitalize-dwim
"M-l" #'downcase-dwim ; "lower" case
"M-u" #'upcase-dwim
;; Commands for object transposition
"C-S-p" #'prot-simple-move-above-dwim
"C-S-n" #'prot-simple-move-below-dwim
@ -157,83 +107,33 @@
;; Commands for paragraphs
"M-Q" #'prot-simple-unfill-region-or-paragraph
;; Commands for windows and pages
"C-x o" #'prot-simple-other-window
"C-x O" #'prot-simple-other-windor-or-frame
"C-x n k" #'prot-simple-delete-page-delimiters
"M-r" #'window-layout-transpose ; Emacs 31 override `move-to-window-line-top-bottom'
"M-S-r" #'rotate-windows-back ; Emacs 31
"C-x M-r" #'prot-simple-swap-window-buffers
;; Commands for buffers
"C-<f2>" #'prot-simple-rename-file-and-buffer
"M-<f2>" #'prot-simple-copy-current-path
"C-x k" #'prot-simple-kill-buffer-dwim
"C-x K" #'kill-buffer ; leaving this here to contrast with the above
"M-=" #'count-words
"<C-f2>" #'prot-simple-rename-file-and-buffer
"C-x k" #'prot-simple-kill-buffer-current
"C-x K" #'kill-buffer
"M-s b" #'prot-simple-buffers-major-mode
"M-s v" #'prot-simple-buffers-vc-root))
;;;; Scratch buffers per major mode (prot-scratch.el)
(prot-emacs-configure
(setq prot-scratch-default-mode 'text-mode)
(autoload #'prot-scratch-buffer "prot-scratch")
(define-key global-map (kbd "C-c s") #'prot-scratch-buffer))
;;;; Insert character pairs (prot-pair.el)
(prot-emacs-configure
(prot-emacs-autoload
(prot-pair-insert prot-pair-insert-directly prot-pair-delete)
"prot-pair")
(prot-emacs-keybind global-map
"C-'" #'prot-pair-insert
"M-'" #'prot-pair-insert-directly
"M-\\" #'prot-pair-delete))
;;;; Comments (prot-comment.el)
(prot-emacs-configure
(setq comment-empty-lines t)
(setq comment-fill-column nil)
(setq comment-multi-line t)
(setq comment-style 'multi-line)
(setq-default comment-column 0)
(setq prot-comment-comment-keywords '("TODO" "NOTE" "FIXME"))
(setq prot-comment-timestamp-format-concise "%F")
(setq prot-comment-timestamp-format-verbose "%F %T %z")
(prot-emacs-autoload
(prot-comment prot-comment-timestamp-keyword)
"prot-comment")
(prot-emacs-keybind global-map
"M-s v" #'prot-simple-buffers-vc-root
;; Scratch buffer for major mode of choice
"C-c s" #'prot-scratch-buffer
;; Comments
"C-;" #'prot-comment
"M-;" #'prot-comment ; overrides `comment-dwim'
"C-x C-;" #'prot-comment-timestamp-keyword))
"C-x C-;" #'prot-comment-timestamp-keyword)
;;;; Prefix keymap (prot-prefix.el)
(prot-emacs-configure
(require 'prot-prefix)
(prot-emacs-keybind global-map
"<insert>" #'prot-prefix
"C-z" #'prot-prefix))
(prot-emacs-keybind prog-mode-map
"C-M-d" #'up-list) ; confusing name for what looks like "down" to me
(prot-emacs-configure
(define-key global-map (kbd "C-x C-r") #'recentf-open) ; override `find-file-read-only'
(with-eval-after-load 'recentf
(setq recentf-max-saved-items 100)
(setq recentf-max-menu-items 25) ; I don't use the `menu-bar-mode', but this is good to know
(setq recentf-save-file-modes nil)
(setq recentf-keep nil)
(setq recentf-auto-cleanup nil)
(setq recentf-initialize-file-name-history nil)
(setq recentf-filename-handlers nil)
(setq recentf-show-file-shortcuts-flag nil)
(recentf-mode 1)))
;; Keymap for buffers (Emacs28)
(prot-emacs-keybind ctl-x-x-map
"f" #'follow-mode ; override `font-lock-update'
"r" #'rename-uniquely
"l" #'visual-line-mode)
;;;; Mouse and mouse wheel behaviour
(prot-emacs-configure
(mouse-wheel-mode 1)
;; Some of these variables are defined in places other than
;; mouse.el, but this is fine.
(setq mouse-autoselect-window t) ; complements the auto-selection of my tiling window manager
(setq focus-follows-mouse t)
;; In Emacs 27+, use Control + mouse wheel to scale text.
(setq mouse-wheel-scroll-amount
@ -247,16 +147,15 @@
mouse-wheel-follow-mouse t)
;; Scrolling behaviour
(setq scroll-error-top-bottom t
scroll-preserve-screen-position t
scroll-conservatively 1 ; affects `scroll-step'
scroll-margin 0
next-screen-context-lines 0))
(setq-default scroll-preserve-screen-position t
scroll-conservatively 1 ; affects `scroll-step'
scroll-margin 0
next-screen-context-lines 0)
(mouse-wheel-mode 1)
(define-key global-map (kbd "C-M-<mouse-3>") #'tear-off-window)
;;;; Repeatable key chords (repeat-mode)
(prot-emacs-configure
(repeat-mode 1)
(setq repeat-on-final-keystroke t
repeat-exit-timeout 5
repeat-exit-key "<escape>"
@ -265,11 +164,10 @@
repeat-echo-function 'ignore
;; Technically, this is not in repeal.el, though it is the
;; same idea.
set-mark-command-repeat-pop t))
set-mark-command-repeat-pop t)
(repeat-mode 1)
;;;; Built-in bookmarking framework (bookmark.el)
(prot-emacs-configure
(add-hook 'bookmark-bmenu-mode-hook #'hl-line-mode)
(setq bookmark-use-annotations nil)
(setq bookmark-automatically-show-annotations nil)
(setq bookmark-fringe-mark nil) ; Emacs 29 to hide bookmark fringe icon
@ -277,35 +175,31 @@
;; made (addition or deletion). Otherwise Emacs will only save the
;; bookmarks when it closes, which may never happen properly
;; (e.g. power failure).
(setq bookmark-save-flag 1))
(setq bookmark-save-flag 1)
;;;; Registers (register.el)
(setq register-preview-delay 0.5
register-preview-function #'register-preview-default)
(add-hook 'bookmark-bmenu-mode-hook #'hl-line-mode)
;;;; Auto revert mode
(prot-emacs-configure
(setq auto-revert-verbose t)
(global-auto-revert-mode 1)
(setq auto-revert-verbose t))
;;;; Delete selection
(prot-emacs-configure
(delete-selection-mode 1))
(delete-selection-mode 1)
;;;; Tooltips (tooltip-mode)
(prot-emacs-configure
(tooltip-mode 1)
(setq tooltip-delay 0.5
tooltip-short-delay 0.5
x-gtk-use-system-tooltips t
x-gtk-use-system-tooltips nil
tooltip-frame-parameters
'((name . "tooltip")
(internal-border-width . 10)
(border-width . 0)
(no-special-glyphs . t))))
(no-special-glyphs . t)))
(autoload #'tooltip-mode "tooltip")
(tooltip-mode 1)
;;;; Display current time
(prot-emacs-configure
(setq display-time-format " %a %e %b, %H:%M ")
;;;; Covered by `display-time-format'
;; (setq display-time-24hr-format t)
@ -330,85 +224,75 @@
'help-echo (format-time-string "%a %b %e, %Y" now))
" "))
(display-time-mode 1))
(display-time-mode 1)
;;;; World clock (M-x world-clock)
(prot-emacs-configure
(setq display-time-world-list t)
(setq zoneinfo-style-world-list ; M-x shell RET timedatectl list-timezones
'(("America/Los_Angeles" "🇺🇸 Los Angeles")
("America/Vancouver" "🇨🇦 Vancouver")
("America/Mexico_City" "🇲🇽 Mexico City")
("America/Denver" "🇺🇸 Denver")
("America/Chicago" "🇺🇸 Chicago")
("America/New_York" "🇺🇸 New York")
("America/Toronto" "🇨🇦 Toronto")
("America/Montreal" "🇨🇦 Montreal")
("UTC" "🌐 UTC")
("Europe/Lisbon" "🇵🇹 Lisbon")
("Europe/London" "🇬🇧 London")
("Europe/Paris" "🇫🇷 Paris")
("Europe/Brussels" "🇧🇪 Brussels")
("Europe/Berlin" "🇩🇪 Berlin")
("Europe/Athens" "🇬🇷 Athens")
("Asia/Nicosia" "🇨🇾 Lefkosia")
("Asia/Istanbul" "🇹🇷 Istanbul")
("Asia/Baghdad" "🇸🇾 Baghdad")
("Asia/Riyadh" "🇸🇦 Riyadh")
("Asia/Tehran" "🇮🇷 Tehran")
("Asia/Tbilisi" "🇬🇪 Tbilisi")
("Asia/Tashkent" "🇺🇿 Tashkent")
("Asia/Karachi" "🇵🇰 Karachi")
("Asia/Kolkata" "🇮🇳 Kolkata")
("Asia/Dhaka" "🇧🇩 Dhaka")
("Asia/Phnom_Penh" "🇰🇭 Phnom Penh")
("Asia/Ho_Chi_Minh" "🇻🇳 Ho Chi Minh")
("Asia/Singapore" "🇸🇬 Singapore")
("Asia/Shanghai" "🇨🇳 Shanghai")
("Asia/Seoul" "🇰🇷 Seoul")
("Asia/Tokyo" "🇯🇵 Tokyo")
("Australia/Brisbane" "🇦🇺 Brisbane")
("Australia/Sydney" "🇦🇺 Sydney")
("Pacific/Auckland" "🇳🇿 Auckland")))
'(("America/Los_Angeles" "Los Angeles")
("America/Vancouver" "Vancouver")
("Canada/Pacific" "Canada/Pacific")
("America/Chicago" "Chicago")
("Brazil/Acre" "Rio Branco")
("America/New_York" "New York")
("Canada/Atlantic" "Canada/Atlantic")
("Brazil/East" "Brasília")
("UTC" "UTC")
("Europe/Lisbon" "Lisbon")
("Europe/Brussels" "Brussels")
("Europe/Athens" "Athens")
("Asia/Riyadh" "Riyadh")
("Asia/Tehran" "Tehran")
("Asia/Tbilisi" "Tbilisi")
("Asia/Yekaterinburg" "Yekaterinburg")
("Asia/Kolkata" "Kolkata")
("Asia/Singapore" "Singapore")
("Asia/Shanghai" "Shanghai")
("Asia/Seoul" "Seoul")
("Asia/Tokyo" "Tokyo")
("Asia/Vladivostok" "Vladivostok")
("Australia/Brisbane" "Brisbane")
("Australia/Sydney" "Sydney")
("Pacific/Auckland" "Auckland")))
;; All of the following variables are for Emacs 28
(setq world-clock-sort-order nil) ; Emacs 31
(setq world-clock-list t)
(setq world-clock-time-format "%z %R %a %d %b (%Z)")
(setq world-clock-time-format "%R %z (%Z) %A %d %B")
(setq world-clock-buffer-name "*world-clock*") ; Placement handled by `display-buffer-alist'
(setq world-clock-timer-enable t)
(setq world-clock-timer-second 60))
(setq world-clock-timer-second 60)
;;;; `man' (manpages)
(prot-emacs-configure
(setq Man-notify-method 'pushy)) ; does not obey `display-buffer-alist'
(setq Man-notify-method 'pushy) ; does not obey `display-buffer-alist'
;;;; `proced' (process monitor, similar to `top')
(prot-emacs-configure
(setq proced-auto-update-flag 'visible) ; Emacs 30 supports more the `visible' value
(setq proced-auto-update-flag t)
(setq proced-enable-color-flag t) ; Emacs 29
(setq proced-auto-update-interval 5)
(setq proced-descend t)
(setq proced-filter 'user))
(setq proced-filter 'user)
;;;; Emacs server (allow emacsclient to connect to running session)
(prot-emacs-configure
(setq server-client-instructions nil)
;; The "server" is functionally like the daemon, except it is run by
;; the first Emacs frame we launch. When we close that frame, the
;; server is terminated. Whereas the daemon remains active even if
;; all Emacs frames are closed.
;;
;; I experimented with the daemon for a while. Emacs would crash
;; whenever I would encounter an error in some Lisp evaluation.
;; Whereas the server works just fine when I need to connect to it via
;; the emacsclient.
(require 'server)
(unless (or (server-running-p) (daemonp))
(setq server-client-instructions nil)
(unless (server-running-p)
(server-start)))
;;; Substitute
;; Another package of mine... Video demo:
;; <https://protesilaos.com/codelog/2023-01-16-emacs-substitute-package-demo/>.
(prot-emacs-configure
(prot-emacs-install substitute)
(require 'substitute)
;; Produce a message after the substitution that reports on what
;; happened. It is a single line, like "Substituted `TARGET' with
;; `SUBSTITUTE' N times across the buffer.
(add-hook 'substitute-post-replace-hook #'substitute-report-operation)
(prot-emacs-package substitute
(:install t)
(:delay 10)
;; Set this to non-nil to highlight all occurrences of the current
;; target.
(setopt substitute-highlight t)
@ -418,105 +302,175 @@
;; argument to do this on-demand.
(setq substitute-fixed-letter-case nil)
;; C-c s is occupied by `prot-scratch-buffer'.
(define-key global-map (kbd "C-c r") #'substitute-prefix-map))
;; Produce a message after the substitution that reports on what
;; happened. It is a single line, like "Substituted `TARGET' with
;; `SUBSTITUTE' N times across the buffer.
(add-hook 'substitute-post-replace-hook #'substitute-report-operation)
(prot-emacs-configure
(prot-emacs-install goto-chg)
;; The mnemonic for the prefix is that M-# (or M-S-3) is close to
;; M-% (or M-S-5).
(prot-emacs-keybind global-map
"C-(" #'goto-last-change
"C-)" #'goto-last-change-reverse))
"M-# s" #'substitute-target-below-point ; Forward motion like isearch (C-s)
"M-# r" #'substitute-target-above-point ; Backward motion like isearch (C-r)
"M-# d" #'substitute-target-in-defun ; "defun" mnemonic
"M-# b" #'substitute-target-in-buffer)) ; "buffer" mnemonic
;;; Mark syntactic constructs efficiently if tree-sitter is available (expreg)
(when (treesit-available-p)
(prot-emacs-package expreg
(:install t)
(:delay 10)
(defun prot/expreg-expand (n)
"Expand to N syntactic units, defaulting to 1 if none is provided interactively."
(interactive "p")
(dotimes (_ n)
(expreg-expand)))
(defun prot/expreg-expand-dwim ()
"Do-What-I-Mean `expreg-expand' to start with symbol or word.
If over a real symbol, mark that directly, else start with a
word. Fall back to regular `expreg-expand'."
(interactive)
(let ((symbol (bounds-of-thing-at-point 'symbol)))
(cond
((equal (bounds-of-thing-at-point 'word) symbol)
(prot/expreg-expand 1))
(symbol (prot/expreg-expand 2))
(t (expreg-expand)))))
;; There is also an `expreg-contract' command, though I have no use for it.
(define-key global-map (kbd "C-M-SPC") #'prot/expreg-expand-dwim))) ; overrides `mark-sexp'
;;; Visualise undo ring (`vundo')
(prot-emacs-package vundo
(:install t)
(:delay 30)
(setq vundo-glyph-alist vundo-unicode-symbols)
(define-key global-map (kbd "C-?") #'vundo) ; override `undo-redo'
;; Check: <https://github.com/casouri/vundo/pull/74>.
(defvar prot/vundo-diff-buffer-window nil
"Window object of `prot/vundo-diff-buffer'.")
(defun prot/vundo-quit-diff-window ()
"Quit `prot/vundo-diff-buffer-window' if it is live.
Assign this function to the `vundo-post-exit-hook'."
(when (and prot/vundo-diff-buffer-window
(window-live-p prot/vundo-diff-buffer-window))
(quit-window nil prot/vundo-diff-buffer-window)
(setq prot/vundo-diff-buffer-window nil)))
(defun prot/vundo-diff-buffer (buffer)
"Diff BUFFER with its underlying file, if possible.
Assign this to `vundo-after-undo-functions'. BUFFER is provided
by that special hook."
(when (buffer-file-name buffer)
(with-current-buffer (window-buffer (diff-buffer-with-file buffer))
(setq prot/vundo-diff-buffer-window (get-buffer-window)))))
(add-hook 'vundo-after-undo-functions #'prot/vundo-diff-buffer)
(add-hook 'vundo-post-exit-hook #'prot/vundo-quit-diff-window))
;;; TMR May Ring (tmr is used to set timers)
;; Read the manual: <https://protesilaos.com/emacs/tmr>.
(prot-emacs-configure
(prot-emacs-install tmr)
(define-key global-map (kbd "C-c t") #'tmr-prefix-map)
(prot-emacs-package tmr
(:install t)
(:delay 15)
(setq tmr-sound-file "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
tmr-notification-urgency 'normal
tmr-description-list 'tmr-description-history))
tmr-description-list 'tmr-description-history)
(prot-emacs-keybind global-map
"C-c t t" #'tmr
"C-c t T" #'tmr-with-description
"C-c t l" #'tmr-tabulated-view ; "list timers" mnemonic
"C-c t c" #'tmr-clone
"C-c t k" #'tmr-cancel
"C-c t s" #'tmr-reschedule
"C-c t e" #'tmr-edit-description
"C-c t r" #'tmr-remove
"C-c t R" #'tmr-remove-finished))
;;; Pass interface (password-store)
(prot-emacs-configure
(prot-emacs-install password-store)
(prot-emacs-package password-store
(:install t)
(:delay 5)
(setq password-store-time-before-clipboard-restore 30)
;; Mnemonic is the root of the "code" word (κώδικας). But also to add
;; the password to the kill-ring. Other options are already taken.
(define-key global-map (kbd "C-c k") #'password-store-copy)
(define-key global-map (kbd "C-c k") #'password-store-copy))
(setq password-store-time-before-clipboard-restore 30)
(prot-emacs-package pass (:install t) (:delay 5))
(prot-emacs-install pass))
;;; Shell (M-x shell)
(prot-emacs-package shell
(:delay 15)
;;; Generic interface for shells or REPLs (comint)
(prot-emacs-configure
;; Support for OS-specific escape sequences such as what `ls
;; --hyperlink' uses. I normally don't use those, but I am checking
;; this to see if there are any obvious advantages/disadvantages.
(add-hook 'comint-output-filter-functions #'comint-osc-process-output)
;; Check my .bashrc which handles `comint-terminfo-terminal':
;;
;; # Default pager. The check for the terminal is useful for Emacs with
;; # M-x shell (which is how I usually interact with bash these days).
;; #
;; # The COLORTERM is documented in (info "(emacs) General Variables").
;; # I found the reference to `dumb-emacs-ansi' in (info "(emacs)
;; # Connection Variables").
;; if [ "$TERM" = "dumb" ] && [ "$INSIDE_EMACS" ]
;; then
;; export PAGER="cat"
;; alias less="cat"
;; export TERM=dumb-emacs-ansi
;; export COLORTERM=1
;; else
;; # Quit once you try to scroll past the end of the file.
;; export PAGER="less --quit-at-eof"
;; fi
(setq ansi-color-for-comint-mode t) ; also see `ansi-color-for-compilation-mode'
(setq shell-command-prompt-show-cwd t) ; Emacs 27.1
(setq ansi-color-for-comint-mode t)
(setq shell-input-autoexpand 'input)
(setq shell-highlight-undef-enable t) ; Emacs 29.1
(setq shell-has-auto-cd nil) ; Emacs 29.1
(setq shell-get-old-input-include-continuation-lines t) ; Emacs 30.1
(setq shell-kill-buffer-on-exit t) ; Emacs 29.1
(setq shell-completion-fignore '("~" "#" "%"))
(setq-default comint-scroll-to-bottom-on-input t)
(setq-default comint-scroll-to-bottom-on-output nil)
(setq-default comint-input-autoexpand 'input)
(setq comint-prompt-read-only t)
(setq comint-buffer-maximum-size 9999)
(setq comint-completion-autolist t)
(setq comint-input-ignoredups t)
(setq-default comint-scroll-to-bottom-on-input t)
(setq-default comint-scroll-to-bottom-on-output nil)
(setq-default comint-input-autoexpand 'input))
(setq tramp-default-remote-shell "/bin/bash")
;;; Compilation interface (M-x compile)
(prot-emacs-configure
(add-hook 'compilation-filter-hook #'ansi-color-compilation-filter)
(setq ansi-color-for-compilation-mode t)) ; also see `ansi-color-for-comint-mode'
(setq shell-font-lock-keywords
'(("[ \t]\\([+-][^ \t\n]+\\)" 1 font-lock-builtin-face)
("^[^ \t\n]+:.*" . font-lock-string-face)
("^\\[[1-9][0-9]*\\]" . font-lock-constant-face)))
;;; Standard Unix Shell (M-x shell)
(prot-emacs-configure
(prot-emacs-autoload (prot-shell prot-shell-mode) "prot-shell")
;; Support for OS-specific escape sequences such as what `ls
;; --hyperlink' uses. I normally don't use those, but I am checking
;; this to see if there are any obvious advantages/disadvantages.
(add-hook 'comint-output-filter-functions 'comint-osc-process-output)
(define-key global-map (kbd "<f1>") #'prot-shell) ; I don't use F1 for help commands
(define-key global-map (kbd "<f1>") #'shell) ; I don't use F1 for help commands
(with-eval-after-load 'prot-shell
(add-hook 'shell-mode-hook #'prot-shell-mode)
(prot-emacs-keybind shell-mode-map
"C-c C-k" #'comint-clear-buffer
"C-c C-w" #'comint-write-output))
(prot-emacs-keybind shell-mode-map
"C-c C-k" #'comint-clear-buffer
"C-c C-w" #'comint-write-output)
(prot-emacs-package prot-shell
(:delay 15)
(add-hook 'shell-mode-hook #'prot-shell-mode))
;; Check my .bashrc which handles `comint-terminfo-terminal':
;;
;; # Default pager. The check for the terminal is useful for Emacs with
;; # M-x shell (which is how I usually interact with bash these days).
;; #
;; # The COLORTERM is documented in (info "(emacs) General Variables").
;; # I found the reference to `dumb-emacs-ansi' in (info "(emacs)
;; # Connection Variables").
;; if [ "$TERM" = "dumb" ] && [ "$INSIDE_EMACS" ] || [ "$TERM" = "dumb-emacs-ansi" ] && [ "$INSIDE_EMACS" ]
;; then
;; export PAGER="cat"
;; alias less="cat"
;; export TERM=dumb-emacs-ansi
;; export COLORTERM=1
;; else
;; # Quit once you try to scroll past the end of the file.
;; export PAGER="less --quit-at-eof"
;; fi
(setq shell-command-prompt-show-cwd t) ; Emacs 27.1
(setq shell-input-autoexpand 'input)
(setq shell-highlight-undef-enable t) ; Emacs 29.1
(setq shell-has-auto-cd nil) ; Emacs 29.1
(setq shell-get-old-input-include-continuation-lines t) ; Emacs 30.1
(setq shell-kill-buffer-on-exit t) ; Emacs 29.1
(setq shell-completion-fignore '("~" "#" "%"))
(setq tramp-default-remote-shell "/bin/bash")
;;; Laptop settings
(unless (directory-empty-p "/sys/class/power_supply/")
(add-to-list 'default-frame-alist '(width . (text-pixels . 800)))
(add-to-list 'default-frame-alist '(height . (text-pixels . 600)))
(setq shell-font-lock-keywords
'(("[ \t]\\([+-][^ \t\n]+\\)" 1 font-lock-builtin-face)
("^[^ \t\n]+:.*" . font-lock-string-face)
("^\\[[1-9][0-9]*\\]" . font-lock-constant-face)))))
;;; Show battery status on the mode line with `display-battery-mode'
(when prot-laptop-p
(prot-emacs-configure
(:delay 10)
;;;; Show battery status on the mode line (battery.el
(require 'battery)
(setq battery-mode-line-format
(cond
@ -524,6 +478,17 @@
"⏻%b%p%%,%d°C ")
(battery-status-function
"⏻%b%p%% ")))
(display-battery-mode 1)))
;;; Get environment variables into Emacs (exec-path-from-shell) while on Wayland
(when-let ((session (getenv "XDG_SESSION_TYPE"))
((equal session "wayland")))
(prot-emacs-package exec-path-from-shell
(:install t)
(:delay 1)
(setq exec-path-from-shell-variables
'("PATH" "MANPATH" "SSH_AUTH_SOCK"))
(exec-path-from-shell-initialize)))
(provide 'prot-emacs-essentials)

View file

@ -0,0 +1,242 @@
(prot-emacs-configure
(:delay 1)
;;;; General settings
(setq evil-want-C-u-delete nil)
(setq evil-want-C-w-delete nil)
(setq evil-want-C-h-delete nil)
(setq evil-want-C-w-in-emacs-state nil)
(setq evil-want-abbrev-expand-on-insert-exit nil)
(setq evil-disable-insert-state-bindings t)
(setq evil-want-change-word-to-end t)
(setq evil-want-C-i-jump t)
(setq evil-want-C-u-scroll t)
(setq evil-want-C-d-scroll t)
(setq evil-want-C-g-bindings nil)
(setq evil-want-Y-yank-to-eol t) ; consistent with D
(setq evil-want-empty-ex-last-command t)
(setq evil-want-integration t)
(setq evil-want-keybinding nil)
(setq evil-want-minibuffer nil)
(setq evil-toggle-key "<f12>") ; I seldom need this, so putting it somewhere far
(setq evil-default-state 'normal) ; check `evil-set-initial-state'
(setq evil-echo-state nil)
(setq evil-mode-line-format nil) ; see `prot-modeline.el' and its implementation in `prot-emacs-modeline.el'
(setq evil-want-fine-undo t)
(setq evil-esc-delay 0.01)
(setq evil-intercept-esc 'always)
(setq evil-highlight-closing-paren-at-point-states '(not emacs insert replace))
(setq evil-kill-on-visual-paste nil) ; Does not work, see `prot-evil-visual-paste-no-kill'
(setq evil-auto-indent t)
(setq evil-undo-system 'undo-redo) ; Emacs 28
(setq evil-visual-update-x-selection-p t)
(setq evil-repeat-move-cursor t)
(setq evil-repeat-find-to-skip-next t)
(setq evil-kbd-macro-suppress-motion-error nil) ; never suppress errors in kmacros, Emacs-style
;; ;; I disable K anyway
;; (setq evil-lookup-func #'ignore)
;; I only use the standard commands for window splitting...
(setq evil-auto-balance-windows window-combination-resize)
(setq evil-split-window-below nil)
(setq evil-vsplit-window-right nil)
(setq evil-mouse-word 'evil-word)
(setq evil-bigword "^ \t\r\n")
;; evil-complete-all-buffer
;; evil-buffer-regexps
;; evil-intercept-maps
;; evil-motions
;; evil-visual-newline-commands
;; evil-text-object-change-visual-type
;; evil-command-window-height
;; evil-display-shell-error-in-message
;;;; Evil search setup
;; NOTE 2023-11-06: These settings do not give me the same isearch
;; experience as core Emacs. I only want that, but to use it with
;; Vim keys.
(setq evil-search-module 'isearch)
(setq evil-symbol-word-search t)
(setq evil-flash-delay 0.5)
(setq evil-ex-hl-update-delay lazy-highlight-initial-delay)
(setq evil-regexp-search nil)
(setq evil-search-wrap t)
(setq evil-search-wrap-ring-bell t) ; TODO does this quit keyboard macros, specifically with C-u 0?
(setq evil-magic nil) ; not applicable for `evil-search-module' isearch module
(setq evil-ex-search-vim-style-regexp nil) ; not for isearch
;; NOTE 2023-11-06: I have not tested all of the following and am
;; not sure which ones are relevant for my use-case.
;;;; Ex commands
;; NOTE 2023-11-06: I have not used any of these...
(setq evil-ex-interactive-search-highlight 'selected-window)
(setq evil-ex-search-persistent-highlight t)
(setq evil-ex-search-case 'smart)
(setq evil-ex-substitute-case nil) ; use `evil-ex-search-case'
(setq evil-ex-search-incremental t)
(setq evil-ex-search-highlight-all t)
(setq evil-ex-substitute-highlight-all t)
(setq evil-ex-substitute-interactive-replace t)
(setq evil-ex-substitute-global nil)
;; evil-ex-complete-emacs-commands 'in-turn
;; evil-ex-visual-char-range nil
;;;; Evil cursors
;; TODO 2023-11-06: integrate with `cursory'
(setq evil-default-cursor t)
;;;; How lines are treated
(setq evil-respect-visual-line-mode nil)
(setq evil-shift-width tab-width)
(setq evil-start-of-line t)
(setq evil-backspace-join-lines t)
(setq evil-cross-lines t)
(setq evil-move-cursor-back nil) ; my "insert" mode is "emacs", so not relevant
(setq evil-move-beyond-eol nil)
(setq evil-show-paren-range 0)
(setq evil-track-eol t)
(setq evil-v$-excludes-newline nil)
;;;; Compatibility with other modes
;; I load the `evil' feature here because some functions/macros of
;; it are needed from this point on. Evil is one of those few
;; packages that requires certain variables to be evaluated before
;; it is `require'd. This is why I place most of the `setq' calls
;; above.
(prot-emacs-package evil (:install t))
;; The `prot-evil' library defines a new state for better
;; compatibility with most (all?) major modes. It also introduces a
;; new "erase" operator. These are configured from this point on.
(prot-emacs-package prot-evil)
(setq evil-overriding-maps nil)
(setq evil-motion-state-modes nil)
(setq evil-insert-state-modes nil)
(setq evil-emacs-state-modes
'(comint-mode
rcirc-mode
eshell-mode
inferior-emacs-lisp-mode
reb-mode
shell-mode
term-mode
wdired-mode
log-edit-mode
git-commit-mode))
(setq evil-prot-basic-state-modes
'(completion-list-mode
Buffer-menu-mode
Custom-mode
edebug-mode
Info-mode
help-mode
diff-mode
ediff-mode
log-view-mode
org-agenda-mode
dired-mode
magit-status-mode
magit-diff-mode
magit-log-mode
notmuch-hello-mode
notmuch-search-mode
notmuch-show-mode
notmuch-tree-mode
special-mode
tabulated-list-mode
world-clock-mode))
(with-eval-after-load 'info
(evil-define-key 'prot-basic Info-mode-map (kbd "L") #'Info-history-back))
(with-eval-after-load 'help-mode
(evil-define-key 'prot-basic help-mode-map (kbd "L") #'help-go-back))
(with-eval-after-load 'dired
(evil-define-key 'prot-basic dired-mode-map (kbd "J") #'dired-goto-file)
(evil-define-key 'prot-basic dired-mode-map (kbd "K") #'dired-do-kill-lines))
(with-eval-after-load 'notmuch
(evil-define-key 'prot-basic notmuch-hello-mode-map (kbd "J") #'notmuch-jump-search)
(evil-define-key 'prot-basic notmuch-show-mode-map (kbd "J") #'notmuch-jump-search)
(evil-define-key 'prot-basic notmuch-search-mode-map (kbd "J") #'notmuch-jump-search)
(evil-define-key 'prot-basic notmuch-tree-mode-map (kbd "J") #'notmuch-jump-search)
(evil-define-key 'prot-basic notmuch-show-mode-map (kbd "K") #'notmuch-tag-jump)
(evil-define-key 'prot-basic notmuch-search-mode-map (kbd "K") #'notmuch-tag-jump)
(evil-define-key 'prot-basic notmuch-tree-mode-map (kbd "K") #'notmuch-tag-jump))
;; See TODO above about the visual state.
(with-eval-after-load 'magit
(evil-define-key '(visual prot-basic) magit-status-mode-map (kbd "K") #'magit-discard)
(evil-define-key 'prot-basic magit-status-mode-map (kbd "l") #'magit-log)
(evil-define-key 'visual magit-status-mode-map (kbd "s") #'magit-stage)
(evil-define-key 'visual magit-status-mode-map (kbd "u") #'magit-unstage))
(with-eval-after-load 'embark
(defun prot-evil-embark-act-or-repeat-pop ()
"Call `evil-repeat-pop' if relevant, else `prot/embark-act-no-quit'."
(interactive)
(call-interactively
(if (memq last-command '(evil-repeat evil-repeat-pop evil-repeat-pop-next))
#'evil-repeat-pop
#'prot/embark-act-no-quit)))
(evil-define-key '(normal visual motion) global-map (kbd "C-.") #'prot-evil-embark-act-or-repeat-pop))
(with-eval-after-load 'org
(evil-define-key '(normal visual motion) org-mode-map
(kbd "<tab>") #'org-cycle
(kbd "<return>") #'org-ctrl-c-ctrl-c))
;;;; Make Emacs the Insert state
(defalias 'evil-insert-state 'evil-emacs-state)
(evil-define-key 'emacs global-map (kbd "<escape>") #'prot-evil-normal-or-basic-state)
(setq evil-emacs-state-cursor evil-insert-state-cursor)
;;;; Custom Evil keys
;; The `prot-evil' library defines an "erase" operator.
(evil-define-key '(normal visual motion) global-map
(kbd "U") #'evil-redo
(kbd "C-r") #'isearch-backward
(kbd "K") #'prot-evil-erase
(kbd "g d") #'xref-find-definitions
(kbd "g D") #'xref-go-back)
;;;; Set up `devil-mode' to reduce modifier key usage
(prot-emacs-package devil (:install t))
(evil-define-key '(normal visual motion prot-basic) global-map (kbd ",") #'devil)
;; This one affects the emacs/insert state.
(global-devil-mode 1)
;;;; Set up my prefix keymap
(evil-define-key '(emacs insert) global-map (kbd "SPC") #'prot-evil-prefix-or-self-insert)
(evil-define-key '(normal motion prot-basic) global-map (kbd "SPC") prot-prefix-map)
;;;; Activate `evil-mode'
(evil-mode 1))
(provide 'prot-emacs-evil)

View file

@ -1,312 +1,251 @@
;;;; `project'
;;; General Git/Project/Diff configurations
(prot-emacs-configure
(:delay 2)
;;;; `ediff'
(setq ediff-keep-variants nil)
(setq ediff-make-buffers-readonly-at-startup nil)
(setq ediff-merge-revisions-with-ancestor t)
(setq ediff-show-clashes-only t)
(setq ediff-split-window-function 'split-window-horizontally)
(setq ediff-window-setup-function 'ediff-setup-windows-plain)
;;;; `project'
(setopt project-switch-commands
'((project-find-file "Find file")
(project-find-regexp "Find regexp")
(project-find-dir "Find directory")
(project-dired "Root dired")
(project-vc-dir "VC-Dir")
(project-shell "Shell")
(keyboard-quit "Quit")))
(setq project-vc-extra-root-markers '(".project")) ; Emacs 29
;; I don't actually have a use-case for `prot-find-project-root',
;; but I wrote it once so I keep it here in case I ever need it.
;; Use it like this: (prot-find-project-root c-mode "Makefile")
(defmacro prot-find-project-root (mode file)
"Define project root check for MODE given FILE.
MODE must be the symbol of the major mode, without a quote. FILE
is a string."
(let ((project-find-fn (intern (format "project-find-%s-root" mode)))
(major-mode-fn (intern (format "bb-%s-project-find-function" mode)))
(file-symbol (intern file)))
`(progn
(defun ,project-find-fn (dir)
(when-let ((root (locate-dominating-file dir ,file)))
(cons ',file-symbol root)))
(cl-defmethod project-root ((project (head ,file-symbol)))
(cdr project))
(defun ,(intern (format "bb-%s-project-find-function" mode)) ()
(add-hook 'project-find-functions #',project-find-fn :depth :local))
(add-hook ',(intern (format "%s-hook" mode)) #',major-mode-fn))))
(defun prot/project--get-key (key cmd)
"Return key binding for KEY, falling back to what CMD is bound to."
(if key
(vector key)
(where-is-internal cmd (list project-prefix-map) t)))
(defun prot/project--keymap-prompt ()
"Return `project-switch-commands' and their fontified key bindings.
Use a newline character to separate each pair of key and command.
Based on `project--keymap-prompt' and meant to be used as an
:override advice for `project--menu-prompt' on Emacs 30."
(mapconcat
(pcase-lambda (`(,cmd ,label ,key))
(let ((key (prot/project--get-key key cmd)))
(format
"%s %s"
(propertize (key-description key) 'face 'help-key-binding)
label)))
project-switch-commands
"\n"))
(advice-add #'project--menu-prompt :override #'prot/project--keymap-prompt)
(advice-add #'project-switch-project :after #'prot-common-clear-minibuffer-message)
(prot-emacs-keybind global-map
"C-x p ." #'project-dired
"C-x p C-g" #'keyboard-quit
"C-x p <return>" #'project-dired
"C-x p <delete>" #'project-forget-project)
(setopt project-switch-commands
'((project-find-file "Find file")
(project-find-dir "Find directory")
(project-find-regexp "Find regexp")
(project-dired "Root dired")
(project-vc-dir "VC-Dir")
(project-shell "Shell")
(project-any-command "Other command")
(keyboard-quit "Quit")))
(setq project-vc-extra-root-markers '(".project")) ; Emacs 29
(setq project-key-prompt-style t) ; Emacs 30
(advice-add #'project-switch-project :after #'prot-common-clear-minibuffer-message)
(autoload #'prot-project-maybe-in-tab "prot-project")
(autoload #'prot-project-switch "prot-project")
(define-key project-prefix-map (kbd "p") #'prot-project-maybe-in-tab))
;;;; `diff-mode'
(prot-emacs-configure
(setq diff-default-read-only t)
(setq diff-advance-after-apply-hunk t)
(setq diff-update-on-the-fly t)
;; The following are from Emacs 27.1
(setq diff-refine nil) ; I do it on demand, with my `agitate' package (more below)
(setq diff-font-lock-prettify t) ; I think nil is better for patches, but let me try this for a while
(setq diff-font-lock-syntax nil))
;;;; `diff-hl'
(prot-emacs-configure
(prot-emacs-install diff-hl)
(setq diff-hl-draw-borders nil)
(setq diff-hl-update-async t)
(prot-emacs-keybind global-map
"C-x v <up>" #'diff-hl-previous-hunk
"C-x v <down>" #'diff-hl-next-hunk)
(prot-emacs-hook (prog-mode-hook vc-dir-mode-hook) turn-on-diff-hl-mode)
(with-eval-after-load 'magit
(add-hook 'magit-post-refresh-hook #'diff-hl-magit-post-refresh)))
;;;; `ediff'
(prot-emacs-configure
(setq ediff-split-window-function 'split-window-horizontally)
(setq ediff-window-setup-function 'ediff-setup-windows-plain)
(setq ediff-keep-variants nil)
(setq ediff-make-buffers-readonly-at-startup nil)
(setq ediff-merge-revisions-with-ancestor t)
(setq ediff-show-clashes-only t)
(with-eval-after-load 'outline
(add-hook 'ediff-prepare-buffer-hook #'outline-show-all))
(prot-emacs-autoload
(prot-ediff-visible-buffers-2
prot-ediff-visible-buffers-3
prot-ediff-store-layout
prot-ediff-restore-layout)
"prot-ediff")
;; The C-x v prefix is for all "version control" commands that are
;; already built into Emacs. It makes sense to extend it for this
;; use-case.
(prot-emacs-keybind global-map
"C-x v 2" #'prot-ediff-visible-buffers-2
"C-x v 3" #'prot-ediff-visible-buffers-3)
(add-hook 'ediff-before-setup-hook #'prot-ediff-store-layout)
(add-hook 'ediff-quit-hook #'prot-ediff-restore-layout))
;;;; `smerge-mode'
(prot-emacs-configure
(setq smerge-diff-buffer-name "*smerge-diff*")
(setq smerge-refine-shadow-cursor nil)) ; Emacs 31
(setq diff-font-lock-syntax 'hunk-also))
;;; Version control framework (vc.el, vc-git.el, and more)
(prot-emacs-configure
(prot-emacs-package vc
(:delay 2)
;; Those offer various types of functionality, such as blaming,
;; viewing logs, showing a dedicated buffer with changes to affected
;; files.
(require 'vc-annotate)
(require 'vc-dir)
(require 'vc-git)
(require 'add-log)
(require 'log-view)
;; I only use Git. If I ever need another, I will include it here.
;; This may have an effect on performance, as Emacs will not try to
;; check for a bunch of backends.
(setq vc-handled-backends '(Git))
;; This one is for editing commit messages.
(require 'log-edit)
(setq log-edit-confirm 'changed)
(setq log-edit-keep-buffer nil)
(setq log-edit-require-final-newline t)
(setq log-edit-setup-add-author nil)
;; I can see the files from the Diff with C-c C-d
(remove-hook 'log-edit-hook #'log-edit-show-files)
(setq vc-find-revision-no-save t)
(setq vc-annotate-display-mode 'scale) ; scale to oldest
;; I use a different account for git commits
(setq add-log-mailing-address "info@protesilaos.com")
(setq add-log-keep-changes-together t)
(setq vc-git-diff-switches '("--patch-with-stat" "--histogram"))
(setq vc-git-log-switches '("--stat"))
(setq vc-git-print-log-follow t)
(setq vc-git-revision-complete-only-branches nil) ; Emacs 28
(setq vc-git-root-log-format
`("%d %h %ai %an: %s"
;; The first shy group matches the characters drawn by --graph.
;; We use numbered groups because `log-view-message-re' wants the
;; revision number to be group 1.
,(concat "^\\(?:[*/\\|]+\\)\\(?:[*/\\| ]+\\)?"
"\\(?2: ([^)]+) \\)?\\(?1:[0-9a-z]+\\) "
"\\(?4:[0-9]\\{4\\}-[0-9-]\\{4\\}[0-9\s+:-]\\{16\\}\\) "
"\\(?3:.*?\\):")
((1 'log-view-message)
(2 'change-log-list nil lax)
(3 'change-log-name)
(4 'change-log-date))))
;; These two are from Emacs 29
(setq vc-git-log-edit-summary-target-len 50)
(setq vc-git-log-edit-summary-max-len 70)
(setq vc-follow-symlinks t)
(with-eval-after-load 'vc
;; Those offer various types of functionality, such as blaming,
;; viewing logs, showing a dedicated buffer with changes to affected
;; files.
(require 'vc-annotate)
(require 'vc-dir)
(require 'vc-git)
(require 'add-log)
(require 'log-view)
;; NOTE: I override lots of the defaults
(prot-emacs-keybind global-map
"C-x v B" #'vc-annotate ; Blame mnemonic
"C-x v e" #'vc-ediff
"C-x v k" #'vc-delete-file ; 'k' for kill==>delete is more common
"C-x v G" #'vc-log-search ; git log --grep
"C-x v t" #'vc-create-tag
"C-x v d" #'vc-diff
"C-x v ." #'vc-dir-root ; `vc-dir-root' is from Emacs 28
"C-x v <return>" #'vc-dir-root)
(prot-emacs-keybind vc-dir-mode-map
"t" #'vc-create-tag
"O" #'vc-log-outgoing
"o" #'vc-dir-find-file-other-window
"d" #'vc-diff ; parallel to D: `vc-root-diff'
"k" #'vc-dir-delete-file
"G" #'vc-revert)
(prot-emacs-keybind vc-git-stash-shared-map
"a" #'vc-git-stash-apply-at-point
"c" #'vc-git-stash ; "create" named stash
"k" #'vc-git-stash-delete-at-point ; symmetry with `vc-dir-delete-file'
"p" #'vc-git-stash-pop-at-point
"s" #'vc-git-stash-snapshot)
(prot-emacs-keybind vc-annotate-mode-map
"M-q" #'vc-annotate-toggle-annotation-visibility
"C-c C-c" #'vc-annotate-goto-line
"<return>" #'vc-annotate-find-revision-at-line)
(prot-emacs-keybind log-edit-mode-map
"M-s" nil ; I use M-s for my search commands
"M-r" nil) ; I use `consult-history'
(prot-emacs-keybind log-view-mode-map
"<tab>" #'log-view-toggle-entry-display
"<return>" #'log-view-find-revision
"s" #'vc-log-search
"o" #'vc-log-outgoing
"f" #'vc-log-incoming
"F" #'vc-update
"P" #'vc-push))
;; I only use Git. If I ever need another, I will include it here.
;; This may have an effect on performance, as Emacs will not try to
;; check for a bunch of backends.
(setq vc-handled-backends '(Git))
;;; Agitate
;; A package of mine to complement VC and friends. Read the manual
;; here: <https://protesilaos.com/emacs/agitate>.
(prot-emacs-package agitate
(:install t)
(:delay 2)
(add-hook 'diff-mode-hook #'agitate-diff-enable-outline-minor-mode)
(advice-add #'vc-git-push :override #'agitate-vc-git-push-prompt-for-remote)
(setq vc-dir-save-some-buffers-on-revert t) ; Emacs 31
(setq vc-use-incoming-outgoing-prefixes t) ; Emacs 31
(setq vc-async-checkin t) ; Emacs 31
(setq vc-display-failed-async-commands t) ; Emacs 31
(setq vc-dir-show-key-binding-hints nil) ; Emacs 32
(setq agitate-log-edit-informative-show-root-log nil
agitate-log-edit-informative-show-files nil)
;; This one is for editing commit messages.
(require 'log-edit)
(setq log-edit-confirm 'changed)
(setq log-edit-keep-buffer nil)
(setq log-edit-require-final-newline t)
(setq log-edit-setup-add-author nil)
(agitate-log-edit-informative-mode 1)
(prot-emacs-hook
log-edit-hook
(log-edit-insert-message-template
log-edit-maybe-show-diff)
nil
log-edit)
(setq vc-find-revision-no-save t)
(setq vc-annotate-display-mode 'scale) ; scale to oldest
;; I use a different account for git commits
(setq add-log-mailing-address "info@protesilaos.com")
(setq add-log-keep-changes-together t)
(setq vc-git-diff-switches '("--patch-with-stat" "--histogram"))
(setq vc-git-log-switches '("--stat"))
(setq vc-git-print-log-follow t)
(setq vc-git-revision-complete-only-branches nil) ; Emacs 28
(setq vc-git-root-log-format
`("%d %h %ai %an: %s"
;; The first shy group matches the characters drawn by --graph.
;; We use numbered groups because `log-view-message-re' wants the
;; revision number to be group 1.
,(concat "^\\(?:[*/\\|]+\\)\\(?:[*/\\| ]+\\)?"
"\\(?2: ([^)]+) \\)?\\(?1:[0-9a-z]+\\) "
"\\(?4:[0-9]\\{4\\}-[0-9-]\\{4\\}[0-9\s+:-]\\{16\\}\\) "
"\\(?3:.*?\\):")
((1 'log-view-message)
(2 'change-log-list nil lax)
(3 'change-log-name)
(4 'change-log-date))))
;; These two are from Emacs 29
(setq vc-git-log-edit-summary-target-len 50)
(setq vc-git-log-edit-summary-max-len 70)
(define-advice vc-push (:around (&rest args) prot)
(let ((current-window (selected-window)))
(apply args)
(select-window current-window)))
(define-advice vc-pull (:around (&rest args) prot)
(let ((current-window (selected-window)))
(apply args)
(select-window current-window)))
(defun prot/vc-diff-dwim ()
"Show diff of buffer against file or against VC history."
(interactive)
(if-let* ((buffer (current-buffer))
(_ (buffer-modified-p buffer)))
(diff-buffer-with-file buffer)
(call-interactively #'vc-diff)))
(defvar prot/vc-git-grep-history nil
"Minibuffer history for `prot/vc-git-grep'.")
(defun prot/vc-git-grep (directory regexp)
"Use `vc-git-grep' with REGEXP in the current root Git DIRECTORY."
(interactive
(let ((directory (or (vc-root-dir)
(locate-dominating-file "." ".git")
(user-error "No VC root available"))))
(list
directory
(read-regexp
(format "vc-git-grep for REGEXP in `%s': "
(propertize directory 'face 'warning))
nil 'prot/vc-git-grep-history))))
(vc-git-grep regexp "*" directory))
;; NOTE: I override lots of the defaults
(prot-emacs-keybind global-map
"C-x v B" #'vc-annotate ; Blame mnemonic
"C-x v g" #'prot/vc-git-grep ; override original `vc-annotate' key
"C-x v e" #'vc-ediff
"C-x v k" #'vc-delete-file ; 'k' for kill==>delete is more common
"C-x v G" #'vc-log-search ; git log --grep
"C-x v t" #'vc-create-tag
"C-x v c" #'vc-clone ; Emacs 31
"C-x v d" #'prot/vc-diff-dwim
"C-x v ." #'vc-dir-root ; `vc-dir-root' is from Emacs 28
"C-x v <return>" #'vc-dir-root)
(prot-emacs-keybind vc-dir-mode-map
"t" #'vc-create-tag
"I" #'vc-log-incoming
"O" #'vc-log-outgoing
"o" #'vc-dir-find-file-other-window
"d" #'vc-diff ; parallel to D: `vc-root-diff'
"k" #'vc-dir-delete-file
"G" #'vc-revert)
(prot-emacs-keybind vc-git-stash-shared-map
"a" #'vc-git-stash-apply-at-point
"c" #'vc-git-stash ; "create" named stash
"k" #'vc-git-stash-delete-at-point ; symmetry with `vc-dir-delete-file'
"p" #'vc-git-stash-pop-at-point
"s" #'vc-git-stash-snapshot)
(prot-emacs-keybind vc-annotate-mode-map
"M-q" #'vc-annotate-toggle-annotation-visibility
"C-c C-c" #'vc-annotate-goto-line
"<return>" #'vc-annotate-find-revision-at-line)
(prot-emacs-keybind log-edit-mode-map
"M-s" nil ; I use M-s for my search commands
"M-r" nil)
(prot-emacs-keybind log-view-mode-map
"<tab>" #'log-view-toggle-entry-display
"<return>" #'log-view-find-revision
"s" #'vc-log-search))
(defun prot/modus-vc-annotate ()
(modus-themes-with-colors
(setq vc-annotate-background-mode nil)
(setq vc-annotate-very-old-color fg-dim)
(setq vc-annotate-color-map
`(( 20. . ,red)
( 40. . ,red-cooler)
( 60. . ,red-warmer)
( 80. . ,yellow-warmer)
(100. . ,yellow)
(120. . ,yellow-cooler)
(140. . ,green-warmer)
(160. . ,green)
(180. . ,green-cooler)
(200. . ,cyan-cooler)
(220. . ,cyan-warmer)
(240. . ,cyan)
(260. . ,blue-warmer)
(280. . ,blue)
(300. . ,blue-cooler)
(320. . ,blue-intense)
(340. . ,magenta-cooler)
(360. . ,fg-dim)))))
(when (memq prot-emacs-load-theme-family '(modus ef standard))
(with-eval-after-load 'vc-annotate
(prot/modus-vc-annotate)
(add-hook 'modus-themes-after-load-theme-hook #'prot/modus-vc-annotate))))
(prot-emacs-keybind global-map
"C-x v =" #'agitate-diff-buffer-or-file ; replace `vc-diff'
"C-x v g" #'agitate-vc-git-grep ; replace `vc-annotate'
"C-x v f" #'agitate-vc-git-find-revision
"C-x v s" #'agitate-vc-git-show
"C-x v w" #'agitate-vc-git-kill-commit-message
"C-x v p p" #'agitate-vc-git-format-patch-single
"C-x v p n" #'agitate-vc-git-format-patch-n-from-head)
(prot-emacs-keybind diff-mode-map
"C-c C-b" #'agitate-diff-refine-cycle ; replace `diff-refine-hunk'
"C-c C-n" #'agitate-diff-narrow-dwim
"L" #'vc-print-root-log
;; Emacs 29 can use C-x v v in diff buffers, which is great, but now I
;; need quick access to it...
"v" #'vc-next-action)
(prot-emacs-keybind log-view-mode-map
"w" #'agitate-log-view-kill-revision
"W" #'agitate-log-view-kill-revision-expanded)
(define-key vc-git-log-view-mode-map (kbd "c") #'agitate-vc-git-format-patch-single)
(prot-emacs-keybind log-edit-mode-map
"C-c C-i C-n" #'agitate-log-edit-insert-file-name
;; See user options `agitate-log-edit-emoji-collection' and
;; `agitate-log-edit-conventional-commits-collection'.
"C-c C-i C-e" #'agitate-log-edit-emoji-commit
"C-c C-i C-c" #'agitate-log-edit-conventional-commit))
;;; Interactive and powerful git front-end (Magit)
(prot-emacs-configure
(setq transient-show-popup 0.5)
(prot-emacs-install magit)
;; Let `display-buffer-alist' do its job
(setq magit-display-buffer-function #'display-buffer)
(define-key global-map (kbd "C-c g") #'magit-status)
(with-eval-after-load 'magit
(prot-emacs-keybind magit-mode-map
"C-w" #'nil
"M-w" #'nil))
;; There is no need to install the package, as transient.el is built
;; into Emacs. By requiring it, I prevent the installation of the
;; package, which would be done by Magit.
(prot-emacs-package transient (:delay 2))
(prot-emacs-package magit
(:install t)
(:delay 10)
(setq magit-define-global-key-bindings nil)
(setq magit-section-visibility-indicators
`(;; (magit-fringe-bitmap> . magit-fringe-bitmapv)
("" . t))) ; same as `org-ellipsis'
;; Show icons for files in the Magit status and other buffers. This
;; is from my prot-icons.el.
(with-eval-after-load 'prot-icons
(setq magit-format-file-function
(lambda (_kind file face &rest _)
(let ((icon (prot-icons-get-file-icon file)))
(format "%s %s" icon (propertize file 'font-lock-face face))))))
(setq magit-diff-refine-hunk t)
(setq magit-diff-fontify-hunk nil)
(setq magit-diff-refine-ignore-whitespace t)
(setq magit-log-auto-more t)
(setq magit-repository-directories
'(("~/Git/Projects" . 1)))
(setq magit-repolist-columns
`(("Name" 25 ,#'magit-repolist-column-ident)
("Version" 15 ,#'magit-repolist-column-version
((:sort magit-repolist-version<)))
("Unpulled" 10 ,#'magit-repolist-column-unpulled-from-upstream
((:help-echo "Upstream changes not in branch")
(:right-align t)
(:sort <)))
("Unpushed" 10 ,#'magit-repolist-column-unpushed-to-upstream
((:help-echo "Local changes not in upstream")
(:right-align t)
(:sort <)))
("Path" 99 ,#'magit-repolist-column-path)))
(setq magit-section-visibility-indicator '(""))
(require 'git-commit)
(setq git-commit-summary-max-length 50)
;; NOTE 2023-01-24: I used to also include `overlong-summary-line'
;; in this list, but I realised I do not need it. My summaries are
;; always in check. When I exceed the limit, it is for a good
;; reason.
(setq git-commit-style-convention-checks '(non-empty-second-line))
(setq git-commit-major-mode #'text-mode))
(require 'magit-diff)
(setq magit-diff-refine-hunk t)
(require 'magit-repos)
(setq magit-repository-directories
'(("~/Git/Projects" . 1)))
(define-key global-map (kbd "C-c g") #'magit-status))
(provide 'prot-emacs-git)

View file

@ -1,11 +1,9 @@
;; This is deprecated code as I stopped using Gnus a long time ago. I
;; keep it here for when I need to test it for my themes (`ef-themes',
;; `modus-themes', `standard-themes', `doric-themes').
;; `modus-themes', `standard-themes').
(use-package gnus
:ensure nil
:config
(prot-emacs-package gnus
(require 'gnus-sum)
(require 'gnus-dired)
(require 'gnus-topic)
@ -129,7 +127,7 @@
;; name, this will use the contents of the "To:" field, prefixed by
;; the string I specify. Useful when checking your "Sent" summary or
;; a mailing list you participate in.
(setq gnus-ignored-from-addresses "Protesilaos")
(setq gnus-ignored-from-addresses "Protesilaos Stavrou")
(setq gnus-summary-to-prefix "To: ")
(setq gnus-summary-line-format "%U%R %-18,18&user-date; %4L:%-25,25f %B%s\n")
@ -172,9 +170,7 @@
(define-key map (kbd "C-M-p") #'gnus-summary-prev-group)
(define-key map (kbd "C-M-^") #'gnus-summary-refer-thread)))
(use-package nnmail
:ensure nil
:config
(prot-emacs-package nnmail
(setq nnmail-expiry-wait 30)) ; careful with this
(provide 'prot-emacs-gnus)

View file

@ -1,46 +1,17 @@
;;; Icons
;; NOTE 2025-12-21: Also see the `prot-minibuffer.el' and my
;; `completion-category-overrides' for completion-related icons.
(prot-emacs-configure
(require 'prot-icons)
(prot-emacs-package nerd-icons
(:install t)
(:delay 5))
(setq prot-icons-style 'emoji) ; `alphabetic', `symbolic', or `emoji'
(prot-emacs-package nerd-icons-completion
(:install t)
(:delay 5)
(nerd-icons-completion-marginalia-setup)
(nerd-icons-completion-mode 1))
(prot-icons-dired-mode 1)
(prot-icons-xref-mode 1)
(when (and prot-emacs-load-theme-family
(eq prot-icons-style 'alphabetic))
(cond
((memq prot-emacs-load-theme-family '(modus ef standard))
(defun prot/icons-set-faces ()
(modus-themes-with-colors
(custom-set-faces
`(prot-icons-icon ((,c :inherit (bold fixed-pitch) :box (:line-width (1 . -1)) :inverse-video t)))
`(prot-icons-directory ((,c :inherit bold :foreground ,accent-0)))
`(prot-icons-gray ((,c :inherit bold :foreground ,fg-dim)))
`(prot-icons-red ((,c :inherit prot-icons-icon :foreground ,red)))
`(prot-icons-green ((,c :inherit prot-icons-icon :foreground ,green-warmer)))
`(prot-icons-yellow ((,c :inherit prot-icons-icon :foreground ,yellow)))
`(prot-icons-blue ((,c :inherit prot-icons-icon :foreground ,blue-cooler)))
`(prot-icons-magenta ((,c :inherit prot-icons-icon :foreground ,magenta-cooler)))
`(prot-icons-cyan ((,c :inherit prot-icons-icon :foreground ,cyan))))))
(add-hook 'modus-themes-after-load-theme-hook #'prot/icons-set-faces))
((eq prot-emacs-load-theme-family 'doric)
(defun prot/icons-set-faces ()
(doric-themes-with-colors
(custom-set-faces
`(prot-icons-icon ((t :inherit (bold fixed-pitch) :box (:line-width (1 . -1)) :inverse-video t)))
`(prot-icons-directory ((t :inherit bold :foreground ,fg-accent)))
`(prot-icons-gray ((t :inherit bold :foreground ,fg-shadow-subtle)))
`(prot-icons-red ((t :inherit prot-icons-icon :foreground ,fg-red)))
`(prot-icons-green ((t :inherit prot-icons-icon :foreground ,fg-green)))
`(prot-icons-yellow ((t :inherit prot-icons-icon :foreground ,fg-yellow)))
`(prot-icons-blue ((t :inherit prot-icons-icon :foreground ,fg-blue)))
`(prot-icons-magenta ((t :inherit prot-icons-icon :foreground ,fg-magenta)))
`(prot-icons-cyan ((t :inherit prot-icons-icon :foreground ,fg-cyan))))))
(add-hook 'doric-themes-after-load-theme-hook #'prot/icons-set-faces)))
(prot/icons-set-faces)))
(prot-emacs-package nerd-icons-dired
(:install t)
(:delay 5)
(add-hook 'dired-mode-hook #'nerd-icons-dired-mode))
(provide 'prot-emacs-icons)

View file

@ -1,400 +1,406 @@
;;; General language/editing settings
(prot-emacs-configure
(:delay 2)
;;;; Tabs, indentation, and the TAB key
(prot-emacs-configure
(setq tab-always-indent 'complete)
(setq tab-first-completion nil) ; Emacs 27
(setq-default tab-width 4
indent-tabs-mode nil))
(setq-default tab-always-indent 'complete
tab-first-completion 'word-or-paren-or-punct ; Emacs 27
tab-width 4
indent-tabs-mode nil)
;;;; Emacs Lisp major mode
(prot-emacs-configure
(with-eval-after-load 'elisp-mode
(prot-emacs-keybind emacs-lisp-mode-map
"C-c C-b" nil ; I do not want to byte compile the buffer
"C-c C-f" nil) ; .. nor the file
(prot-emacs-keybind lisp-interaction-mode-map
"C-c C-b" nil
"C-c C-f" nil)
;; All of these are from Emacs 31.
(setq elisp-eldoc-funcall-with-docstring-length 'short)
(setq elisp-eldoc-docstring-length-limit 1000)
(set-default-toplevel-value 'lexical-binding t) ; Emacs 31
(dolist (package prot-emacs-my-packages)
(add-to-list 'elisp-flymake-byte-compile-load-path (expand-file-name (format "%s" package) "/home/prot/Git/Projects/")))
(require 'prot-elisp)
(prot-emacs-keybind emacs-lisp-mode-map
"C-j" #'prot-elisp-eval-and-print-last-sexp ; overrides `electric-newline-and-maybe-indent'
"C-c C-p" #'prot-elisp-pp-macroexpand-last-sexp
"C-M-w" #'prot-elisp-copy-current-definition) ; overrides `append-next-kill'
(prot-emacs-keybind lisp-interaction-mode-map
"C-j" #'prot-elisp-eval-and-print-last-sexp ; overrides `eval-print-last-sexp'
"C-c C-p" #'prot-elisp-pp-macroexpand-last-sexp
"C-M-w" #'prot-elisp-copy-current-definition))) ; overrides `append-next-kill'
;;;; The "electric" behaviour
(prot-emacs-configure
(electric-pair-mode 1)
(electric-indent-mode 1)
;;;; Disable "electric" behaviour
(electric-pair-mode -1)
(electric-quote-mode -1)
(defun prot/electric-indent-mode-disable ()
"Disable `electric-indent-local-mode'."
(electric-indent-local-mode -1))
(add-hook 'org-mode-hook #'prot/electric-indent-mode-disable))
;; I don't like auto indents in Org and related. They are okay for
;; programming.
(electric-indent-mode -1)
(add-hook 'prog-mode-hook #'electric-indent-local-mode)
;;;; Parentheses (show-paren-mode)
(prot-emacs-configure
(add-hook 'prog-mode-hook #'show-paren-local-mode)
(with-eval-after-load 'paren
(setq show-paren-style 'parenthesis)
(setq show-paren-when-point-in-periphery nil)
(setq show-paren-when-point-inside-paren nil)
(setq show-paren-context-when-offscreen 'overlay) ; Emacs 29
(setq show-paren-not-in-comments-or-strings nil))) ; Emacs 31
(setq show-paren-style 'parenthesis)
(setq show-paren-when-point-in-periphery nil)
(setq show-paren-when-point-inside-paren nil)
(setq show-paren-context-when-offscreen 'overlay) ; Emacs 29
(add-hook 'after-init-hook #'show-paren-mode)
;;;; Emacs Lisp (emacs-lisp-mode)
(prot-emacs-keybind emacs-lisp-mode-map
"C-x e" #'edebug-defun ; override `kmacro-end-and-call-macro'
"C-x E" #'edebug-remove-instrumentation)
;;;; Plain text (text-mode)
(prot-emacs-configure
(add-to-list 'auto-mode-alist '("\\`\\(README\\|CHANGELOG\\|COPYING\\|LICENSE\\)\\'" . text-mode))
(setq sentence-end-double-space nil)
(defun prot/prog-mode-sentence-end-double-space ()
"Set `sentence-end-double-space' to non-nil in the current buffer.
Meant to be added to `prog-mode-hook'."
(setq-local sentence-end-double-space t))
(add-hook 'prog-mode-hook #'prot/prog-mode-sentence-end-double-space)
(setq sentence-end-without-period nil)
(setq colon-double-space nil)
(setq use-hard-newlines nil)
(setq adaptive-fill-mode t)
(add-hook 'text-mode-hook #'turn-on-auto-fill)
(add-hook 'prog-mode-hook (lambda () (setq-local sentence-end-double-space t)))
(with-eval-after-load 'text-mode
(setq sentence-end-double-space nil)
(setq sentence-end-without-period nil)
(setq colon-double-space nil)
(setq use-hard-newlines nil)
(setq adaptive-fill-mode t)))
(add-to-list 'auto-mode-alist '("\\(README\\|CHANGELOG\\|COPYING\\|LICENSE\\)\\'" . text-mode))
(prot-emacs-configure
;; Arch Linux and AUR package scripts (sh-mode)
;;;; Arch Linux and AUR package scripts (sh-mode)
(add-to-list 'auto-mode-alist '("PKGBUILD" . sh-mode))
;; SystemD and other configuration files (conf-mode)
(add-to-list 'auto-mode-alist '("\\`dircolors\\'" "\\.\\(service\\|timer\\)\\'" "dunstrc")))
;;;; Eldoc (Emacs live documentation feedback)
(prot-emacs-configure
(add-hook 'prog-mode-hook #'eldoc-mode)
(setq eldoc-idle-delay 1.0)
(setq eldoc-message-function #'message)) ; don't use mode line for M-x eval-expression, etc.
;;;; SystemD and other configuration files (conf-mode)
(add-to-list 'auto-mode-alist '("\\.\\(service\\|timer\\)\\'" . conf-mode))
(add-to-list 'auto-mode-alist '("dircolors" . conf-mode))
;;;; Eldoc (elisp live documentation feedback)
(setq eldoc-message-function #'message) ; don't use mode line for M-x eval-expression, etc.
(global-eldoc-mode 1)
;;;; Eglot (built-in client for the language server protocol)
(prot-emacs-configure
(setq eglot-sync-connect nil)
(setq eglot-autoshutdown t))
(setq eglot-autoshutdown t)
;;;; Handle performance for very long lines (so-long.el)
(prot-emacs-configure
(global-so-long-mode 1))
;;; Markdown (markdown-mode)
(prot-emacs-configure
(prot-emacs-install markdown-mode)
(prot-emacs-package markdown-mode
(:install t)
(:delay 5)
(add-to-list 'auto-mode-alist '("\\.md\\'" . markdown-mode))
(setq markdown-fontify-code-blocks-natively t))
;;; csv-mode
(prot-emacs-configure
(prot-emacs-install csv-mode))
;;; SXHKDRC mode (one of my many packages)
(prot-emacs-configure
(prot-emacs-install sxhkdrc-mode)
(prot-emacs-package sxhkdrc-mode
(:install t)
(:delay 5)
;; By default, it only applies to the sxhkdrc file, but I have other
;; relevant entries as well. I separate my keys into different
;; modules and load only what I need.
(add-to-list 'auto-mode-alist '("sxhkdrc_.*" . sxhkdrc-mode)))
;; FIXME 2023-08-28: The laptop I use now that I do not have
;; electricity/Internet at home does not build the Jinx C module. I
;; am not sure what is happening. Commenting out for the time being,
;; as I need to have Emacs up and running.
;;; Jinx (highly performant spell checker)
;; (if (executable-find "enchant-2")
;; (prot-emacs-package jinx
;; (:install t)
;; (:delay 10)
;; (setq jinx-languages "en_GB el_GR fr_FR es_ES pt_PT-preao")
;; (setq jinx-include-modes '(text-mode prog-mode))
;; (setq jinx-include-faces
;; '((prog-mode font-lock-doc-face)
;; (conf-mode font-lock-comment-face)))
;; (setq jinx-exclude-regexps
;; '((t "[A-Z]+\\>"
;; "\\<[[:upper:]][[:lower:]]+\\>"
;; "\\w*?[0-9\.'\"-]\\w*"
;; "[a-z]+://\\S-+"
;; "<?[-+_.~a-zA-Z][-+_.~:a-zA-Z0-9]*@[-.a-zA-Z0-9]+>?")))
;;
;; (global-jinx-mode 1)
;;
;; (define-key ctl-x-x-map "j" #'jinx-mode) ; C-x x j
;;
;; (prot-emacs-keybind global-map
;; "M-$" jinx-correct
;; "C-M-$" jinx-languages))
;; ;; I would use an `error' but I do not want it to interrupt startup.
;; (message "libenchant is not available"))
;;; Flyspell and prot-spell.el (spell check)
(prot-emacs-configure
(prot-emacs-autoload
(prot-spell-spell-dwim
prot-spell-change-dictionary
prot-spell-spell-dwim
prot-spell-change-dictionary
prot-spell-french-punctuation)
"prot-spell")
;; See FIXME for `jinx'.
(prot-emacs-package flyspell
(:delay 30)
(setq flyspell-issue-message-flag nil)
(setq flyspell-issue-welcome-flag nil)
(setq ispell-program-name "aspell")
(setq ispell-dictionary "en_GB")
(define-key flyspell-mode-map (kbd "C-;") nil)
(define-key ctl-x-x-map "s" flyspell-mode)) ; C-x x s
(prot-emacs-package prot-spell
(:delay 30)
(setq prot-spell-dictionaries
'(("EN English" . "en")
("EL Ελληνικά" . "el")
("FR Français" . "fr")
("ES Espanõl" . "es")))
(setq ispell-choices-buffer "*ispell-top-choices*") ; see my `display-buffer-alist'
;; Also check prot-spell.el for what I am doing with
;; `prot-spell-ispell-display-buffer'. Then refer to the
;; `display-buffer-alist' for the relevant entry.
(prot-emacs-keybind global-map
"M-$" #'prot-spell-spell-dwim
"C-M-$" #'prot-spell-change-dictionary
"M-i" #'prot-spell-spell-dwim ; override `tab-to-tab-stop
"C-M-i" #'prot-spell-change-dictionary) ; override `complete-symbol'
(with-eval-after-load 'flyspell
(define-key flyspell-mode-map (kbd "C-;") nil)
(define-key flyspell-mouse-map (kbd "<mouse-3>") #'flyspell-correct-word)
(define-key ctl-x-x-map (kbd "s") #'flyspell-mode) ; C-x x s
(setq flyspell-issue-message-flag nil)
(setq flyspell-issue-welcome-flag nil)
(setq ispell-program-name "aspell")
(setq ispell-dictionary "en_GB")
(setq prot-spell-dictionaries
'(("EN English" . "en")
("EL Ελληνικά" . "el")
("FR Français" . "fr")
("ES Espanõl" . "es")))
;; Also check prot-spell.el for what I am doing with
;; `prot-spell-ispell-display-buffer'. Then refer to the
;; `display-buffer-alist' for the relevant entry.
(setq ispell-choices-buffer "*ispell-top-choices*")))
"C-M-$" #'prot-spell-change-dictionary))
;;; Flymake
(prot-emacs-configure
(defun prot/flymake-mode-in-my-projects ()
"Enable `flymake-mode' when editing a trusted file."
(when (and buffer-file-name
(not (file-directory-p buffer-file-name))
(file-regular-p buffer-file-name)
(seq-some
(lambda (directory-or-file)
(string-prefix-p (expand-file-name directory-or-file) (expand-file-name buffer-file-name)))
trusted-content))
(prot-emacs-package flymake
(:delay 30)
(setq flymake-fringe-indicator-position 'left-fringe)
(setq flymake-suppress-zero-counters t)
(setq flymake-start-on-flymake-mode t)
(setq flymake-no-changes-timeout nil)
(setq flymake-start-on-save-buffer t)
(setq flymake-proc-compilation-prevents-syntax-check t)
(setq flymake-wrap-around nil)
(setq flymake-mode-line-format
'("" flymake-mode-line-exception flymake-mode-line-counters))
;; NOTE 2023-07-03: `prot-modeline.el' actually defines the counters
;; itself and ignores this.
(setq flymake-mode-line-counter-format
'("" flymake-mode-line-error-counter
flymake-mode-line-warning-counter
flymake-mode-line-note-counter ""))
(setq flymake-show-diagnostics-at-end-of-line nil) ; Emacs 30
(defvar prot/flymake-mode-projects-path
(file-name-as-directory (expand-file-name "Projects" "~/Git/"))
"Path to my Git projects.")
(defun prot/flymake-mode-lexical-binding ()
(when lexical-binding
(flymake-mode 1)))
(add-hook 'prog-mode-hook #'prot/flymake-mode-in-my-projects)
(defun prot/flymake-mode-in-my-projects ()
(when-let* ((file (buffer-file-name))
((string-prefix-p prot/flymake-mode-projects-path
(expand-file-name file)))
((not (file-directory-p file)))
((file-regular-p file)))
(add-hook 'find-file-hook #'prot/flymake-mode-lexical-binding nil t)))
(define-key ctl-x-x-map (kbd "m") #'flymake-mode) ; C-x x m
(add-hook 'emacs-lisp-mode-hook #'prot/flymake-mode-in-my-projects)
(with-eval-after-load 'flymake
(prot-emacs-keybind flymake-mode-map
"C-c ! s" #'flymake-start
"C-c ! d" #'flymake-show-buffer-diagnostics ; Emacs 28
"C-c ! D" #'flymake-show-project-diagnostics ; Emacs 28
"C-c ! n" #'flymake-goto-next-error
"C-c ! p" #'flymake-goto-prev-error)
(setq flymake-fringe-indicator-position 'left-fringe)
(setq flymake-suppress-zero-counters t)
(setq flymake-no-changes-timeout nil)
(setq flymake-start-on-flymake-mode t)
(setq flymake-start-on-save-buffer t)
(setq flymake-proc-compilation-prevents-syntax-check t)
(setq flymake-wrap-around nil)
(setq flymake-mode-line-format
'("" flymake-mode-line-exception flymake-mode-line-counters))
;; NOTE 2023-07-03: `prot-modeline.el' actually defines the counters
;; itself and ignores this.
(setq flymake-mode-line-counter-format
'("" flymake-mode-line-error-counter
flymake-mode-line-warning-counter
flymake-mode-line-note-counter ""))
(setq flymake-inline-diagnostics ; Emacs 32
'((current . fancy)
(current-line . short)
(t . nil))))
(define-key ctl-x-x-map "m" #'flymake-mode) ; C-x x m
(prot-emacs-keybind flymake-mode-map
"C-c ! s" #'flymake-start
"C-c ! d" #'flymake-show-buffer-diagnostics ; Emacs28
"C-c ! D" #'flymake-show-project-diagnostics ; Emacs28
"C-c ! n" #'flymake-goto-next-error
"C-c ! p" #'flymake-goto-prev-error))
;;; Elisp packaging requirements
(prot-emacs-install package-lint-flymake)
(package-lint-flymake-setup))
(prot-emacs-package package-lint-flymake
(:install t)
(:delay 30)
(add-hook 'flymake-diagnostic-functions #'package-lint-flymake))
;;; General configurations for prose/writing
;;;; `outline' (`outline-mode' and `outline-minor-mode')
(prot-emacs-configure
(define-key global-map (kbd "<f10>") #'outline-minor-mode)
;;;; `outline' (`outline-mode' and `outline-minor-mode')
(:delay 10)
(setq outline-minor-mode-highlight nil) ; emacs28
(setq outline-minor-mode-cycle t) ; emacs28
(setq outline-minor-mode-cycle t) ; emacs28
(setq outline-minor-mode-use-buttons nil) ; emacs29---bless you for the nil option!
(setq outline-minor-mode-use-margins nil)) ; as above
(setq outline-minor-mode-use-margins nil) ; as above
(define-key global-map (kbd "<f10>") #'outline-minor-mode)
;;;; `docview' (simple PDF viewer)
;; The "mupdf" is a reference to the Arch Linux system packages
;; `mupdf', `mupdf-tools', `libmupdf'.
(setq doc-view-pdf->png-converter-function #'doc-view-pdf->png-converter-mupdf)
(setq doc-view-mupdf-use-svg (image-type-available-p 'svg)) ; Emacs 30
(setq doc-view-resolution 300) ; (doc-view-clear-cache)
;;;; `dictionary'
(prot-emacs-configure
(define-key global-map (kbd "C-c d") #'dictionary-search)
(setq dictionary-server "dict.org")
(setq dictionary-default-popup-strategy "lev") ; read doc string
(setq dictionary-create-buttons nil)
(setq dictionary-use-single-buffer t))
(setq dictionary-server "dict.org"
dictionary-default-popup-strategy "lev" ; read doc string
dictionary-create-buttons nil
dictionary-use-single-buffer t)
(define-key global-map (kbd "C-c d") #'dictionary-search))
;;; aLtCaPs
;; Read the manual: <https://protesilaos.com/emacs/altcaps>.
(prot-emacs-configure
(prot-emacs-install altcaps)
(define-key global-map (kbd "C-x C-a") #'altcaps-dwim)
(prot-emacs-package altcaps
(:install t)
(:delay 60)
;; Force letter casing for certain characters (for legibility).
(setq altcaps-force-character-casing
'(;; Greek theta
( . downcase))))
( . downcase)))
;; The available commands: `altcaps-word', `altcaps-region',
;; `altcaps-dwim'.
(define-key global-map (kbd "C-x C-a") #'altcaps-dwim))
;;; Denote (simple note-taking and file-naming)
;; Read the manual: <https://protesilaos.com/emacs/denote>.
(prot-emacs-package denote
(:install t)
(:delay 5)
;; Remember to check the doc strings of those variables.
(setq denote-directory (expand-file-name "~/Documents/notes/"))
(setq denote-file-type 'text) ; Org is the default, set others here like I do
(setq denote-file-name-letter-casing
'((signature . verbatim)
(title . downcase)
(keywords . verbatim)
(t , verbatim)))
;; If you want to have a "controlled vocabulary" of keywords,
;; meaning that you only use a predefined set of them, then you want
;; `denote-infer-keywords' to be nil and `denote-known-keywords' to
;; have the keywords you need.
(setq denote-known-keywords '("emacs" "philosophy" "politics" "economics"))
(setq denote-infer-keywords t)
(setq denote-sort-keywords t)
(setq denote-excluded-directories-regexp nil)
(setq denote-date-format nil) ; read its doc string
(setq denote-rename-no-confirm t)
(setq denote-backlinks-show-context nil)
(setq denote-rename-buffer-format "[D] %t")
;; Read the manual: <https://protesilaos.com/emacs/denote>. This does
;; not include all the useful features of Denote. I have a separate
;; private setup for those, as I need to test everything is in order.
(prot-emacs-configure
(prot-emacs-install denote)
(add-hook 'text-mode-hook #'denote-fontify-links-mode)
;; Automatically rename Denote buffers when opening them so that
;; instead of their long file name they have a literal "[D]"
;; followed by the file's title. Read the doc string of
;; `denote-rename-buffer-format' for how to modify this.
(denote-rename-buffer-mode 1)
;; If you use Markdown or plain text files you want to buttonise
;; existing buttons upon visiting the file (Org renders links as
;; buttons right away).
(add-hook 'find-file-hook #'denote-link-buttonize-buffer)
;; Highlight Denote file names in Dired buffers. Below is the
;; generic approach, which is great if you rename files Denote-style
;; in lots of places as I do:
(add-hook 'dired-mode-hook #'denote-dired-mode)
;;
;; OR if you only want the `denote-dired-mode' in select
;; directories, then modify the variable `denote-dired-directories'
;; and use the following instead:
;;
;; (add-hook 'dired-mode-hook #'denote-dired-mode-in-directories)
(require 'denote-journal-extras)
(setq denote-journal-extras-directory nil) ; use the `denote-directory'
(setq denote-journal-extras-title-format nil) ; always prompt for title
(setq denote-journal-extras-keyword "journal")
;; Denote DOES NOT define any key bindings. This is for the user to
;; decide. For example:
(prot-emacs-keybind global-map
"C-c n n" #'denote
"C-c n N" #'denote-type
"C-c n d" #'denote-dired
"C-c n g" #'denote-grep
"C-c n d" #'denote-date
"C-c n z" #'denote-signature ; "zettelkasten" mnemonic
"C-c n s" #'denote-subdirectory
"C-c n o" #'denote-sort-dired ; "order" mnemonic
"C-c n j" #'denote-journal-extras-new-entry
"C-c n J" #'denote-journal-extras-new-or-existing-entry
;; Note that `denote-rename-file' can work from any context, not
;; just Dired buffers. That is why we bind it here to the
;; `global-map'.
;;
;; Also see `denote-rename-file-using-front-matter' further below.
"C-c n r" #'denote-rename-file)
(with-eval-after-load 'text-mode
(prot-emacs-keybind text-mode-map
"C-c n i" #'denote-link ; "insert" mnemonic
"C-c n I" #'denote-add-links
"C-c n b" #'denote-backlinks
"C-c n R" #'denote-rename-file-using-front-matter))
;; If you intend to use Denote with a variety of file types, it is
;; easier to bind the link-related commands to the `global-map', as
;; shown here. Otherwise follow the same pattern for
;; `org-mode-map', `markdown-mode-map', and/or `text-mode-map'.
(prot-emacs-keybind text-mode-map
"C-c n i" #'denote-link ; "insert" mnemonic
"C-c n I" #'denote-add-links
"C-c n b" #'denote-backlinks
"C-c n f f" #'denote-find-link
"C-c n f b" #'denote-find-backlink
;; Also see `denote-rename-file' further above.
"C-c n R" #'denote-rename-file-using-front-matter)
(with-eval-after-load 'dired
(prot-emacs-keybind dired-mode-map
"C-c C-d C-i" #'denote-dired-link-marked-notes
"C-c C-d C-r" #'denote-dired-rename-marked-files
"C-c C-d C-k" #'denote-dired-rename-marked-files-with-keywords
"C-c C-d C-f" #'denote-dired-rename-marked-files-using-front-matter))
;; I do not bind the Org dynamic blocks, but they are useful:
;;
;; - `denote-org-dblock-insert-links'
;; - `denote-org-dblock-insert-backlinks'
;; - `denote-org-dblock-insert-files'
(with-eval-after-load 'denote
(setq denote-directory (expand-file-name "~/Documents/notes/"))
(setq denote-file-type 'text) ; Org is the default file type
;; Key bindings specifically for Dired.
(prot-emacs-keybind dired-mode-map
"C-c C-d C-i" #'denote-link-dired-marked-notes
"C-c C-d C-r" #'denote-dired-rename-marked-files
"C-c C-d C-k" #'denote-dired-rename-marked-files-with-keywords
"C-c C-d C-f" #'denote-dired-rename-marked-files-using-front-matter)
(setq denote-known-keywords '("emacs" "philosophy" "politics"))
(setq denote-infer-keywords t)
(setq denote-sort-keywords t)
(setq denote-excluded-directories-regexp nil)
(setq denote-date-format nil)
(setq denote-rename-confirmations nil) ; CAREFUL with this if you are not familiar with Denote!
(setq denote-backlinks-show-context nil)
(setq denote-buffer-name-prefix "[D] ")
(setq denote-rename-buffer-format "%D")
(with-eval-after-load 'org-capture
(setq denote-org-capture-specifiers "%l\n%i\n%?")
(add-to-list 'org-capture-templates
'("n" "New note (with denote.el)" plain
(file denote-last-path)
#'denote-org-capture
:no-save t
:immediate-finish nil
:kill-buffer t
:jump-to-captured t))
(denote-rename-buffer-mode 1)
;; ;; EXPERIMENT 2025-04-25: This is not the default order, though
;; ;; Denote supports any order for its file name components.
;; (setq denote-file-name-components-order '(identifier signature keywords title))
(defun prot/denote-rename-all-to-reorder-components ()
"Call `denote-dired-rename-files' without any prompts.
In other words, preserve the value of each Denote file name component.
Use this command if you want to modify the user option
`denote-file-name-components-order' and then want your files to
retroactively follow that order."
(interactive)
(let ((denote-prompts nil))
(call-interactively 'denote-dired-rename-files)))))
;;;; Integrate Consult with Denote
(when prot-emacs-completion-extras
(prot-emacs-configure
(prot-emacs-install consult-denote)
(prot-emacs-keybind global-map
"C-c n f" #'consult-denote-find
"C-c n g" #'consult-denote-grep)
(consult-denote-mode 1)))
;;;; Denote Org extras (denote-org)
(prot-emacs-configure
(prot-emacs-install denote-org))
;;;; Denote Sequence notes or folgezettel (denote-sequence)
(prot-emacs-configure
(prot-emacs-install denote-sequence)
(prot-emacs-keybind global-map
"C-c n s s" #'denote-sequence
"C-c n s f" #'denote-sequence-find
"C-c n s l" #'denote-sequence-link
"C-c n s d" #'denote-sequence-dired
"C-c n s r" #'denote-sequence-reparent
"C-c n s c" #'denote-sequence-convert)
(setq denote-sequence-scheme 'alphanumeric))
;;;; Denote Markdown extras (denote-markdown)
(prot-emacs-configure
(prot-emacs-install denote-markdown))
;;;; Denote Journal extras (denote-journal)
(prot-emacs-configure
(prot-emacs-install denote-journal)
(add-hook 'calendar-mode-hook #'denote-journal-calendar-mode)
(defun prot/denote-journal-new-or-existing-entry ()
"EXPERIMENTAL Like `denote-journal-new-or-existing-entry' but with no front matter."
(interactive)
(cl-letf (((symbol-function #'denote--format-front-matter) (lambda (&rest _) ""))
(denote-file-type 'text)
(denote-journal-title-format ""))
(let* ((internal-date (current-time))
(files (denote-journal--entry-today internal-date)))
(if files
(find-file (denote-journal-select-file-prompt files))
(call-interactively 'denote-journal-new-entry)))))
(define-key global-map (kbd "C-c n j") #'prot/denote-journal-new-or-existing-entry)
(with-eval-after-load 'denote
(setq denote-journal-directory (expand-file-name "journal" denote-directory))
(setq denote-journal-keyword "journal")
(setq denote-journal-title-format 'day-date-month-year)))
;;;; Denote Silo extras (denote-silo)
(prot-emacs-configure
(prot-emacs-install denote-silo)
(with-eval-after-load 'denote
(setq denote-silo-directories
(append (denote-directories)
(list "~/Documents/books/" "~/Documents/denote-test-silo/")))))
;; This prompts for TITLE, KEYWORDS, and SUBDIRECTORY
(add-to-list 'org-capture-templates
'("N" "New note with prompts (with denote.el)" plain
(file denote-last-path)
(function
(lambda ()
(denote-org-capture-with-prompts :title :keywords :signature)))
:no-save t
:immediate-finish nil
:kill-buffer t
:jump-to-captured t))))
;;; Custom extensions for "focus mode" (logos.el)
;; Read the manual: <https://protesilaos.com/emacs/logos>.
(prot-emacs-configure
(prot-emacs-install olivetti)
(setq-default olivetti-body-width 0.7)
(prot-emacs-package olivetti
(:install t)
(:delay 10)
(setq olivetti-body-width 0.7)
(setq olivetti-minimum-body-width 80)
(setq olivetti-recall-visual-line-mode-entry-state t)
(setq olivetti-recall-visual-line-mode-entry-state t))
(prot-emacs-install logos)
(prot-emacs-package logos
(:install t)
(:delay 10)
(setq logos-outlines-are-pages t)
(setq logos-outline-regexp-alist
`((emacs-lisp-mode . ,(format "\\(^;;;+ \\|%s\\)" logos-page-delimiter))
(org-mode . ,(format "\\(^\\*+ +\\|^-\\{5\\}$\\|%s\\)" logos-page-delimiter))
(markdown-mode . ,(format "\\(^\\#+ +\\|^[*-]\\{5\\}$\\|^\\* \\* \\*$\\|%s\\)" logos-page-delimiter))
(conf-toml-mode . "^\\[")))
;; These apply when `logos-focus-mode' is enabled. Their value is
;; buffer-local.
(setq-default logos-hide-mode-line t)
(setq-default logos-hide-header-line t)
(setq-default logos-hide-buffer-boundaries t)
(setq-default logos-hide-fringe t)
(setq-default logos-variable-pitch t) ; see my `fontaine' configurations
(setq-default logos-buffer-read-only nil)
(setq-default logos-scroll-lock nil)
(setq-default logos-olivetti t)
;; I don't need to do `with-eval-after-load' for the `modus-themes' as
;; I always load them before other relevant potentially packages.
(add-hook 'modus-themes-post-load-theme-hook #'logos-update-fringe-in-buffers)
(prot-emacs-keybind global-map
"C-x n n" #'logos-narrow-dwim
"C-x ]" #'logos-forward-page-dwim
"C-x [" #'logos-backward-page-dwim
;; I don't think I ever saw a package bind M-] or M-[...
"M-]" #'logos-forward-page-dwim
"M-[" #'logos-backward-page-dwim
"M-n" #'logos-forward-page-dwim
"M-p" #'logos-backward-page-dwim
"<f9>" #'logos-focus-mode)
(with-eval-after-load 'logos
(setq logos-outlines-are-pages t)
(setq logos-outline-regexp-alist
`((emacs-lisp-mode . ,(format "\\(^;;;+ \\|%s\\)" logos-page-delimiter))
(org-mode . ,(format "\\(^\\*+ +\\|^-\\{5\\}$\\|%s\\)" logos-page-delimiter))
(markdown-mode . ,(format "\\(^\\#+ +\\|^[*-]\\{5\\}$\\|^\\* \\* \\*$\\|%s\\)" logos-page-delimiter))
(conf-toml-mode . "^\\[")))
;; These apply when `logos-focus-mode' is enabled. Their value is
;; buffer-local.
(setq-default logos-hide-mode-line t)
(setq-default logos-hide-header-line t)
(setq-default logos-hide-buffer-boundaries t)
(setq-default logos-hide-fringe t)
(setq-default logos-variable-pitch t) ; see my `fontaine' configurations
(setq-default logos-buffer-read-only nil)
(setq-default logos-scroll-lock nil)
(setq-default logos-olivetti t)
(add-hook 'enable-theme-functions #'logos-update-fringe-in-buffers)
;;;; Extra tweaks
;; place point at the top when changing pages, but not in `prog-mode'
(defun prot/logos-recenter-top ()
"Use `recenter' to reposition the view at the top."
(unless (derived-mode-p 'prog-mode)
(recenter 1))) ; Use 0 for the absolute top
;; place point at the top when changing pages, but not in `prog-mode'
(defun prot/logos--recenter-top ()
"Use `recenter' to reposition the view at the top."
(unless (derived-mode-p 'prog-mode)
(recenter 1))) ; Use 0 for the absolute top
(add-hook 'logos-page-motion-hook #'prot/logos-recenter-top)))
(add-hook 'logos-page-motion-hook #'prot/logos--recenter-top))
(provide 'prot-emacs-langs)

View file

@ -1,61 +1,72 @@
;;; Minibuffer and Completions in Tandem or Minibuffer Confines Transcended (mct)
;;; Minibuffer and Completions in Tandem
;; Read the manual: <https://protesilaos.com/emacs/mct>.
(prot-emacs-configure
(prot-emacs-install mct)
(setq mct-hide-completion-mode-line t)
(setq mct-completing-read-multiple-indicator t)
(prot-emacs-package mct
(:install t)
(:delay 1)
(setq completions-format 'one-column)
(setq mct-hide-completion-mode-line nil)
;; The blocklist and passlist accept either commands/functions or
;; completion categories.
(setq mct-completion-blocklist '(notmuch-mua-new-mail notmuch-mua-prompt-for-sender))
(setq mct-completion-passlist
'(;; Some commands
prot-search-outline
select-frame-by-name
Info-goto-node
Info-index
Info-menu
vc-retrieve-tag
;; Some completion categories
consult-buffer
consult-location
embark-keybinding
imenu
file
project-file
buffer
kill-ring))
(setq mct-completion-passlist '(consult-buffer consult-location embark-keybinding imenu select-frame-by-name))
(setq mct-remove-shadowed-file-names t)
(setq mct-completion-window-size (cons #'mct-frame-height-third 1))
(setq mct-persist-dynamic-completion nil)
(setq mct-live-completion 'visible)
;; This is the default value but I am keeping it here in case I
;; choose to modify it.
(setq mct-sort-by-command-or-category
'((file . mct-sort-by-directory-then-by-file)
((magit-checkout vc-retrieve-tag) . mct-sort-by-alpha-then-by-length)
((kill-ring imenu consult-location Info-goto-node Info-index Info-menu) . nil) ; no sorting
(t . mct-sort-by-history)))
(mct-mode 1)
;; Adaptation of `icomplete-fido-backward-updir'.
(defun prot/mct-backward-updir ()
"Delete char before point or go up a directory."
(interactive nil mct-mode)
(cond
((and (eq (char-before) ?/)
(eq (mct--completion-category) 'file))
(when (string-equal (minibuffer-contents) "~/")
(delete-minibuffer-contents)
(insert (expand-file-name "~/"))
(goto-char (line-end-position)))
(save-excursion
(goto-char (1- (point)))
(when (search-backward "/" (minibuffer-prompt-end) t)
(delete-region (1+ (point)) (point-max)))))
(t (call-interactively 'backward-delete-char))))
(defvar prot/mct-commands-with-line-numbers
'( consult-line consult-line-multi consult-mark
consult-outline consult-grep consult-ripgrep)
"List of commands that are known to show contextual line numbers.")
(define-key minibuffer-local-filename-completion-map (kbd "DEL") #'prot/mct-backward-updir)
(defun prot/mct-display-line-numbers ()
"Call `display-line-numbers-mode' when it is not confusing.
(mct-mode 1))
Check if the current command is among the list of
`prot/mct-commands-with-line-numbers'. The elements of that list
are known to display their own contextual lines numbers.
Add this to `completion-list-mode-hook'."
(unless (memq this-command prot/mct-commands-with-line-numbers)
(display-line-numbers-mode)))
(add-hook 'completion-list-mode-hook #'prot/mct-display-line-numbers)
;; Specify the sorting function.
(setq completions-sort #'mct-sort-multi-category)
(defun prot/mct-next-line-or-completion (n)
"Select next completion or move to next line N times.
Select the next completion if `completion-in-region-mode' is
active and the Completions window is on display."
(interactive "p")
(if (and completion-in-region-mode (mct--get-completion-window))
(minibuffer-next-completion n)
(next-line n)))
(defun prot/mct-previous-line-or-completion (n)
"Select previous completion or move to previous line N times.
Select the previous completion if `completion-in-region-mode' is
active and the Completions window is on display."
(interactive "p")
(if (and completion-in-region-mode (mct--get-completion-window))
(minibuffer-previous-completion n)
(previous-line n)))
(defun prot/mct-return-or-choose-completion (n)
"Choose current completion or create N newlines.
Choose the current completion if `completion-in-region-mode' is
active and the Completions window is on display."
(interactive "p")
(if (and completion-in-region-mode (mct--get-completion-window))
(minibuffer-choose-completion)
(newline n :interactive)))
(prot-emacs-keybind completion-in-region-mode-map
"C-n" #'prot/mct-next-line-or-completion
"C-p" #'prot/mct-previous-line-or-completion
"RET" #'prot/mct-return-or-choose-completion))
(provide 'prot-emacs-mct)

View file

@ -1,25 +1,20 @@
;;; Mode line
(prot-emacs-configure
(require 'prot-modeline)
(prot-emacs-package prot-modeline
(:delay 1)
(setq mode-line-compact nil) ; Emacs 28
(setq mode-line-right-align-edge 'right-margin) ; Emacs 30
(setq prot-modeline-show-frame-name (alist-get 'undecorated initial-frame-alist))
(setq-default mode-line-format
'("%e"
prot-modeline-kbd-macro
prot-modeline-narrow
prot-modeline-buffer-status
prot-modeline-window-dedicated-status
prot-modeline-input-method
" "
prot-modeline-evil
prot-modeline-buffer-identification
" "
prot-modeline-major-mode
prot-modeline-process
" "
prot-modeline-frame-name
" "
prot-modeline-vc-branch
" "
prot-modeline-eglot
@ -27,55 +22,15 @@
prot-modeline-flymake
" "
mode-line-format-right-align ; Emacs 30
prot-modeline-which-function-indicator
prot-modeline-notmuch-indicator
" "
prot-modeline-misc-info))
(when prot-emacs-load-theme-family
(cond
((memq prot-emacs-load-theme-family '(modus ef standard))
(defun prot/modeline-set-faces ()
(modus-themes-with-colors
(custom-set-faces
`(prot-modeline-indicator-red ((,c :inherit bold :foreground ,red)))
`(prot-modeline-indicator-green ((,c :inherit bold :foreground ,green)))
`(prot-modeline-indicator-yellow ((,c :inherit bold :foreground ,yellow)))
`(prot-modeline-indicator-blue ((,c :inherit bold :foreground ,blue)))
`(prot-modeline-indicator-magenta ((,c :inherit bold :foreground ,magenta)))
`(prot-modeline-indicator-cyan ((,c :inherit bold :foreground ,cyan)))
`(prot-modeline-indicator-red-bg ((,c :inherit (bold prot-modeline-indicator-button) :background ,bg-red-intense :foreground ,fg-main)))
`(prot-modeline-indicator-green-bg ((,c :inherit (bold prot-modeline-indicator-button) :background ,bg-green-intense :foreground ,fg-main)))
`(prot-modeline-indicator-yellow-bg ((,c :inherit (bold prot-modeline-indicator-button) :background ,bg-yellow-intense :foreground ,fg-main)))
`(prot-modeline-indicator-blue-bg ((,c :inherit (bold prot-modeline-indicator-button) :background ,bg-blue-intense :foreground ,fg-main)))
`(prot-modeline-indicator-magenta-bg ((,c :inherit (bold prot-modeline-indicator-button) :background ,bg-magenta-intense :foreground ,fg-main)))
`(prot-modeline-indicator-cyan-bg ((,c :inherit (bold prot-modeline-indicator-button) :background ,bg-cyan-intense :foreground ,fg-main))))))
(add-hook 'modus-themes-after-load-theme-hook #'prot/modeline-set-faces))
((eq prot-emacs-load-theme-family 'doric)
(defun prot/modeline-set-faces ()
(doric-themes-with-colors
(custom-set-faces
`(prot-modeline-indicator-red ((t :inherit bold :foreground ,fg-red)))
`(prot-modeline-indicator-green ((t :inherit bold :foreground ,fg-green)))
`(prot-modeline-indicator-yellow ((t :inherit bold :foreground ,fg-yellow)))
`(prot-modeline-indicator-blue ((t :inherit bold :foreground ,fg-blue)))
`(prot-modeline-indicator-magenta ((t :inherit bold :foreground ,fg-magenta)))
`(prot-modeline-indicator-cyan ((t :inherit bold :foreground ,fg-cyan)))
`(prot-modeline-indicator-red-bg ((t :inherit (bold prot-modeline-indicator-button) :background ,bg-red :foreground ,fg-main)))
`(prot-modeline-indicator-green-bg ((t :inherit (bold prot-modeline-indicator-button) :background ,bg-green :foreground ,fg-main)))
`(prot-modeline-indicator-yellow-bg ((t :inherit (bold prot-modeline-indicator-button) :background ,bg-yellow :foreground ,fg-main)))
`(prot-modeline-indicator-blue-bg ((t :inherit (bold prot-modeline-indicator-button) :background ,bg-blue :foreground ,fg-main)))
`(prot-modeline-indicator-magenta-bg ((t :inherit (bold prot-modeline-indicator-button) :background ,bg-magenta :foreground ,fg-main)))
`(prot-modeline-indicator-cyan-bg ((t :inherit (bold prot-modeline-indicator-button) :background ,bg-cyan :foreground ,fg-main))))))
(add-hook 'doric-themes-after-load-theme-hook #'prot/modeline-set-faces)))
(prot/modeline-set-faces))
(with-eval-after-load 'spacious-padding
(defun prot/modeline-spacious-indicators ()
"Set box attribute to `'prot-modeline-indicator-button' if spacious-padding is enabled."
(if (bound-and-true-p spacious-padding-mode)
(set-face-attribute 'prot-modeline-indicator-button nil :box '(:style flat-button))
(set-face-attribute 'prot-modeline-indicator-button nil :box t)
(set-face-attribute 'prot-modeline-indicator-button nil :box 'unspecified)))
;; Run it at startup and then afterwards whenever
@ -84,27 +39,19 @@
(add-hook 'spacious-padding-mode-hook #'prot/modeline-spacious-indicators)))
;;; Show the name of the current definition or heading for context (`which-function-mode')
(prot-emacs-configure
(setq which-func-modes '(prog-mode org-mode))
;; NOTE 2025-10-26: I handle the indicator on my own via `prot-modeline-which-function-indicator'.
(setq which-func-display 'mode) ; Emacs 30
(setq which-func-unknown "")
(which-function-mode 1))
;;; Keycast mode
(prot-emacs-configure
(prot-emacs-install keycast)
(with-eval-after-load 'prot-modeline
(setq keycast-mode-line-format "%2s%k%c%R")
(setq keycast-mode-line-insert-after 'prot-modeline-vc-branch)
(setq keycast-mode-line-window-predicate 'mode-line-window-selected-p)
(setq keycast-mode-line-remove-tail-elements nil))
(with-eval-after-load 'keycast
(dolist (input '(self-insert-command org-self-insert-command))
(add-to-list 'keycast-substitute-alist `(,input "." "Typing…")))
(prot-emacs-package keycast
(:install t)
(:delay 60)
(setq keycast-mode-line-format "%2s%k%c%R")
(setq keycast-mode-line-insert-after 'prot-modeline-vc-branch)
(setq keycast-mode-line-window-predicate 'mode-line-window-selected-p)
(setq keycast-mode-line-remove-tail-elements nil)
(dolist (event '("<mouse-event>" "<mouse-movement>" "<mouse-2>" "<drag-mouse-1>" "<wheel-up>" "<wheel-down>" "<double-wheel-up>" "<double-wheel-down>" "<triple-wheel-up>" "<triple-wheel-down>" "<wheel-left>" "<wheel-right>" handle-select-window mouse-set-point mouse-drag-region))
(add-to-list 'keycast-substitute-alist `(,event nil nil)))))
(dolist (input '(self-insert-command org-self-insert-command))
(add-to-list 'keycast-substitute-alist `(,input "." "Typing…")))
(dolist (event '(mouse-event-p mouse-movement-p mwheel-scroll))
(add-to-list 'keycast-substitute-alist `(,event nil))))
(provide 'prot-emacs-modeline)

View file

@ -2,69 +2,86 @@
;; The themes are highly customisable. Read the manual:
;; <https://protesilaos.com/emacs/modus-themes>.
(prot-emacs-configure
(prot-emacs-install modus-themes)
;; Starting with version 5.0.0 of the `modus-themes', other packages
;; can be built on top to provide their own "Modus" derivatives.
;; For example, this is what I do with my `ef-themes' and
;; `standard-themes' (starting with versions 2.0.0 and 3.0.0,
;; respectively).
;;
;; The `modus-themes-include-derivatives-mode' makes all Modus
;; commands that act on a theme consider all such derivatives, if
;; their respective packages are available and have been loaded.
;;
;; Note that those packages can even completely take over from the
;; Modus themes such that, for example, `modus-themes-rotate' only
;; goes through the Ef themes (to this end, the Ef themes provide
;; the `ef-themes-take-over-modus-themes-mode' and the Standard
;; themes have the `standard-themes-take-over-modus-themes-mode'
;; equivalent).
;;
;; If you only care about the Modus themes, then (i) you do not need
;; to enable the `modus-themes-include-derivatives-mode' and (ii) do
;; not install and activate those other theme packages.
(modus-themes-include-derivatives-mode 1)
(prot-emacs-keybind global-map
"<f5>" #'modus-themes-rotate
"C-<f5>" #'modus-themes-select
"M-<f5>" #'modus-themes-load-random)
(setq modus-themes-mixed-fonts t
(prot-emacs-package modus-themes
(:install t)
(:delay 1)
(setq modus-themes-custom-auto-reload nil
modus-themes-to-toggle '(modus-operandi modus-vivendi)
;; modus-themes-to-toggle '(modus-operandi-tinted modus-vivendi-tinted)
;; modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia)
;; modus-themes-to-toggle '(modus-operandi-tritanopia modus-vivendi-tritanopia)
modus-themes-mixed-fonts t
modus-themes-variable-pitch-ui t
modus-themes-italic-constructs t
modus-themes-bold-constructs t
modus-themes-completions '((t . (bold)))
modus-themes-prompts '(bold)
modus-themes-bold-constructs nil
modus-themes-org-blocks nil
modus-themes-completions '((t . (extrabold)))
modus-themes-prompts '(extrabold)
modus-themes-headings
'((agenda-structure . (variable-pitch light 2.2))
(agenda-date . (variable-pitch regular 1.3))
(t . (regular 1.15))))
(setq modus-themes-common-palette-overrides nil)
;; '((bg-mode-line-active bg-cyan-subtle)
;; (keybind yellow-warmer)))
(if (prot-emacs-gnome-prefers-dark-p)
(modus-themes-load-random 'dark)
(modus-themes-load-random 'light)))
(if (prot-emacs-theme-environment-dark-p)
(modus-themes-load-theme (cadr modus-themes-to-toggle))
(modus-themes-load-theme (car modus-themes-to-toggle)))
;; NOTE: For testing purposes
(prot-emacs-comment
(:eval nil)
;; Also check `modus-themes-select'.
(define-key global-map (kbd "<f5>") #'modus-themes-toggle))
(add-to-list 'load-path "~/Git/Projects/modus-themes/")
(require 'modus-themes)
(modus-themes-include-derivatives-mode 1)
(prot-emacs-keybind global-map
"<f5>" #'modus-themes-rotate
"C-<f5>" #'modus-themes-select
"M-<f5>" #'modus-themes-load-random)
(if (prot-emacs-gnome-prefers-dark-p)
(modus-themes-load-random 'dark)
(modus-themes-load-random 'light)))
;; ;; NOTE: For testing purposes
;; (progn
;; (mapc #'disable-theme custom-enabled-themes)
;;
;; (add-to-list 'load-path "/home/prot/Git/Projects/modus-themes/")
;;
;; (require 'modus-themes)
;;
;; (setq modus-themes-custom-auto-reload nil
;; modus-themes-to-toggle '(modus-operandi modus-vivendi)
;; ;; modus-themes-to-toggle '(modus-operandi-tinted modus-vivendi-tinted)
;; ;; modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia)
;; ;; modus-themes-to-toggle '(modus-operandi-tritanopia modus-vivendi-tritanopia)
;; modus-themes-mixed-fonts t
;; modus-themes-variable-pitch-ui nil
;; modus-themes-italic-constructs t
;; modus-themes-bold-constructs t
;; modus-themes-org-blocks nil
;; modus-themes-completions '((t . (extrabold)))
;; modus-themes-prompts nil
;; modus-themes-headings
;; '((agenda-structure . (variable-pitch light 2.2))
;; (agenda-date . (variable-pitch regular 1.3))
;; (t . (regular 1.15))))
;;
;; ;; (setq modus-themes-common-palette-overrides nil)
;;
;; (setq modus-themes-common-palette-overrides
;; `((fringe unspecified)
;; ;; (bg-mode-line-active bg-lavender)
;; ;; (border-mode-line-active unspecified)
;; ;; (border-mode-line-inactive unspecified)
;; (bg-line-number-active bg-hl-line)
;; (bg-line-number-inactive unspecified)
;; (fg-line-number-active fg-main)
;; ;; ,@modus-themes-preset-overrides-warmer
;; ))
;;
;; ;; ;; Make the active mode line have a pseudo 3D effect (this assumes
;; ;; ;; you are using the default mode line and not an extra package).
;; ;; (custom-set-faces
;; ;; '(mode-line ((t :box (:style unspecified)))))
;;
;; (if (prot-emacs-theme-environment-dark-p)
;; (modus-themes-load-theme (cadr modus-themes-to-toggle))
;; (modus-themes-load-theme (car modus-themes-to-toggle)))
;;
;; ;; Also check `modus-themes-select'. To list the palette's colours,
;; ;; use `modus-themes-list-colors', `modus-themes-list-colors-current'.
;; (define-key global-map (kbd "<f5>") #'modus-themes-toggle))
(provide 'prot-emacs-modus-themes)

View file

@ -1,234 +1,120 @@
;; This is deprecated code as I stopped using mu4e a long time ago. I
;; keep it here for when I need to test it for my themes (`ef-themes',
;; `modus-themes', `standard-themes', `doric-themes').
;; `modus-themes', `standard-themes').
(use-package mu4e
:ensure nil
;; This is an exception because I install it from the system
;; distribution's package archives (depends on non-Emacs code)
:load-path "/usr/share/emacs/site-lisp/elpa-src/mu4e-1.8.14/"
:config
(setq mu4e-maildir "~/.20240226-mail-mu4e")
(setq mu4e-attachment-dir (expand-file-name "~/Downloads/mail-attachments/"))
(setq mu4e-confirm-quit nil)
(setq mu4e-mu-allow-temp-file t) ; mu 1.12.0
(setq mu4e-use-fancy-chars t ; Cool idea, but they create misalignments
mu4e-headers-draft-mark '("D" . "⚒️")
mu4e-headers-flagged-mark '("F" . "🚩")
mu4e-headers-new-mark '("N" . "🔥")
mu4e-headers-passed-mark '("P" . "📨")
mu4e-headers-replied-mark '("R" . "✏️")
mu4e-headers-seen-mark '("S" . "👁️‍🗨️")
mu4e-headers-trashed-mark '("T" . "🚫")
mu4e-headers-attach-mark '("a" . "📎")
mu4e-headers-encrypted-mark '("x" . "🔒")
mu4e-headers-signed-mark '("s" . "🔑")
mu4e-headers-unread-mark '("u" . "💬")
mu4e-headers-list-mark '("l" . "📬")
mu4e-headers-personal-mark '("p" . "🦚")
mu4e-headers-calendar-mark '("c" . "📅"))
;; This is an exception because I install it from the Arch Linux
;; package archives (depends on non-Emacs code)
(add-to-list 'load-path "/usr/share/emacs/site-lisp/mu4e/")
(require 'mu4e)
;; (setq mail-user-agent 'mu4e-user-agent)
(setq mu4e-use-fancy-chars nil)
(setq mu4e-headers-advance-after-mark t)
(setq mu4e-headers-auto-update t)
(setq mu4e-headers-date-format "%F")
(setq mu4e-headers-time-format "%R")
(setq mu4e-headers-long-date-format "%F, %R")
(setq mu4e-marks
'((refile
:char ("r" . "")
:prompt "refile"
:dyn-target (lambda (target msg) (mu4e-get-refile-folder msg))
:action (lambda (docid msg target)
(mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
(delete
:char ("D" . "🚫")
:prompt "Delete"
:show-target (lambda (target) "delete")
:action (lambda (docid msg target) (mu4e--server-remove docid)))
(flag
:char ("+" . "🚩")
:prompt "+flag"
:show-target (lambda (target) "flag")
:action (lambda (docid msg target)
(mu4e--server-move docid nil "+F-u-N")))
(move
:char ("m" . "")
:prompt "move"
:ask-target mu4e--mark-get-move-target
:action (lambda (docid msg target)
(mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
(read
:char ("!" . "👁️‍🗨️")
:prompt "!read"
:show-target (lambda (target) "read")
:action (lambda (docid msg target) (mu4e--server-move docid nil "+S-u-N")))
(trash
:char ("d" . "🚫")
:prompt "dtrash"
:dyn-target (lambda (target msg) (mu4e-get-trash-folder msg))
:action (lambda (docid msg target)
(mu4e--server-move docid
(mu4e--mark-check-target target) "+T-N")))
(unflag
:char ("-" . "")
:prompt "-unflag"
:show-target (lambda (target) "unflag")
:action (lambda (docid msg target) (mu4e--server-move docid nil "-F-N")))
(untrash
:char ("=" . "")
:prompt "=untrash"
:show-target (lambda (target) "untrash")
:action (lambda (docid msg target) (mu4e--server-move docid nil "-T")))
(unread
:char ("?" . "💬")
:prompt "?unread"
:show-target (lambda (target) "unread")
:action (lambda (docid msg target) (mu4e--server-move docid nil "-S+u-N")))
(unmark
:char " "
:prompt "unmark"
:action (mu4e-error "No action for unmarking"))
(action
:char ( "a" . "")
:prompt "action"
:ask-target (lambda () (mu4e-read-option "Action: " mu4e-headers-actions))
:action (lambda (docid msg actionfunc)
(save-excursion
(when (mu4e~headers-goto-docid docid)
(mu4e-headers-action actionfunc)))))
(something
:char ("*" . "")
:prompt "*something"
:action (mu4e-error "No action for deferred mark"))))
(setq mu4e-headers-fields
'((:human-date . 12)
(:flags . 6)
(:mailing-list . 10)
(:from . 22)
(:subject)))
(setq mu4e-modeline-support t
mu4e-modeline-unread-items '("U:" . "[U]")
mu4e-modeline-all-read '("R:" . "[R]")
mu4e-modeline-all-clear '("C:" . "[C]")
mu4e-modeline-max-width 42)
(setq mu4e-get-mail-command "true")
(setq mu4e-hide-index-messages t)
(setq mu4e-update-interval (* 60 5))
(setq mu4e-completing-read-function 'completing-read)
(setq mu4e-compose-signature "Protesilaos Stavrou\nprotesilaos.com\n")
(setq mu4e-compose-signature-auto-include t)
(setq mu4e-maildir "~/.mail20220814T112445")
(setq mu4e-attachment-dir "~/Downloads")
(setq mu4e-sent-messages-behavior 'sent)
(setq mu4e-view-show-addresses t)
(setq mu4e-context-policy 'pick-first)
(setq mu4e-compose-context-policy 'ask)
(setq mu4e-index-lazy-check t)
(setq mu4e-change-filenames-when-moving t) ; better for `mbsync'?
(setq mu4e-modeline-max-width 30)
(setq mu4e-display-update-status-in-modeline t)
(setq mu4e-view-show-images nil)
(setq mu4e-decryption-policy 'ask)
(setq mu4e-headers-include-related t)
(setq mu4e-view-auto-mark-as-read t)
(setq mu4e-notification-support t
;; TODO 2024-02-26: Write custom mu4e notification function.
mu4e-notification-filter #'mu4e--default-notification-filter)
(setq mu4e-contexts
`(,(make-mu4e-context
:name "vrp" ; Is there no way to specify a key for switching?
:enter-func (lambda () (mu4e-message "Entering PRV"))
:leave-func (lambda () (mu4e-message "Leaving PRV"))
:match-func (lambda (msg)
(when msg
(mu4e-message-contact-field-matches
msg :to (prot-common-auth-get-field "prv" :user))))
:vars `((user-mail-address . ,(prot-common-auth-get-field "prv" :user))
;; (mu4e-trash-folder . "/relpath/to/Trash")
;; (mu4e-sent-folder . "/relpath/to/Sent")
))
,(make-mu4e-context
:name "inf"
:match-func (lambda (msg)
(when msg
(mu4e-message-contact-field-matches
msg :to (prot-common-auth-get-field "inf" :user))))
:vars `((user-mail-address . ,(prot-common-auth-get-field "inf" :user))))
,(make-mu4e-context
:name "pub"
:match-func (lambda (msg)
(when msg
(mu4e-message-contact-field-matches
msg :to (prot-common-auth-get-field "pub" :user))))
:vars `((user-mail-address . ,(prot-common-auth-get-field "pub" :user))))))
(setq mu4e-headers-advance-after-mark nil)
(setq mu4e-headers-auto-update t)
(setq mu4e-headers-date-format "%F %a, %T")
(setq mu4e-headers-time-format "%R")
(setq mu4e-headers-long-date-format "%F, %R")
(setq mu4e-headers-leave-behavior 'apply)
(setq mu4e-bookmarks
'((:name "Unread messages" :query "g:unread AND NOT g:trashed" :key ?u)
(:name "Today's messages" :query "d:today..now" :key ?t)
(:name "Last 7 days" :query "d:7d..now" :key ?w)
(:name "PRV Unread"
:query `,(format "to:%s %s"
(prot-common-auth-get-field "prv" :user)
"AND g:unread AND NOT g:trashed")
:key ?v)
(:name "PRV Inbox"
:query `,(format "to:%s"
(prot-common-auth-get-field "prv" :user))
:key ?V)
(:name "INF Unread"
:query `,(format "to:%s %s"
(prot-common-auth-get-field "inf" :user)
"AND g:unread AND NOT g:trashed")
:key ?i)
(:name "INF Inbox"
:query `,(format "to:%s"
(prot-common-auth-get-field "inf" :user))
:key ?I)
(:name "PUB Unread"
:query `,(format "to:%s %s"
(prot-common-auth-get-field "pub" :user)
"AND g:unread AND NOT g:trashed")
:key ?p)
(:name "PUB Inbox"
:query `,(format "to:%s"
(prot-common-auth-get-field "pub" :user))
:key ?P)))
(setq mu4e-headers-fields
'((:date . 26)
(:flags . 8)
(:from . 20)
(:subject)))
(define-key global-map (kbd "C-c M") #'mu4e)
(setq mu4e-get-mail-command "true" ; I auto-fetch with a systemd timer
mu4e-update-interval nil)
(setq mu4e-hide-index-messages t)
(setq mu4e-read-option-use-builtin nil
mu4e-completing-read-function 'completing-read)
(setq mu4e-search-results-limit -1
mu4e-search-sort-field :date
mu4e-search-sort-direction 'descending)
(setq mu4e-org-support t)
(setq mu4e-sent-messages-behavior 'sent)
(setq mu4e-view-show-addresses t)
(setq mu4e-split-view 'horizontal)
(setq mu4e-index-lazy-check t)
(setq mu4e-change-filenames-when-moving t) ; better for `mbsync'?
(setq mu4e-display-update-status-in-modeline nil)
(setq mu4e-view-show-images nil)
(setq mu4e-headers-include-related nil)
(setq mu4e-view-auto-mark-as-read t)
(setq mu4e-compose-complete-addresses nil
mu4e-compose-complete-only-personal t)
(setq mu4e-compose-signature "Protesilaos\nprotesilaos.com\n")
(setq mu4e-compose-signature-auto-include t)
(setq mu4e-context-policy 'pick-first
mu4e-compose-context-policy nil)
(setq mu4e-contexts
`(,@(mapcar
(lambda (context)
(let ((address (prot-common-auth-get-field context :user)))
(make-mu4e-context
:name context
:match-func `(lambda (msg)
(when msg
(mu4e-message-contact-field-matches msg :to ,address)))
:vars `((user-mail-address . ,address)
(mu4e-trash-folder . ,(format "/%s/Trash" context))
(mu4e-sent-folder . ,(format "/%s/Sent" context))))))
'("pub" "inf" "prv"))))
;; 2024-02-26 10:34 +0200 WORK-IN-PROGRESS
(setq mu4e-bookmarks
`((:name "All unread messages" :query "g:unread AND NOT g:trashed" :key ?a)
(:name "All messages" :query "*" :key ?A)
( :name "Personal unread"
:query "contact:/@protesilaos/ or contact:protesilaos AND g:unread AND NOT contact:/@.*gnu/"
:key ?u
:favorite t)
( :name "Personal inbox"
:query "contact:/@protesilaos/ or contact:protesilaos AND NOT contact:/@.*gnu/"
:key ?U
:favorite t)
( :name "Mailing list unread"
:query "contact:/@.*gnu/ AND g:unread"
:key ?m)
( :name "Mailing list inbox"
:query "contact:/@.*gnu/"
:key ?M)))
(defun prot/mu4e (&rest args)
(cl-letf (((symbol-function 'display-buffer-full-frame) #'display-buffer-same-window))
(apply args)))
(advice-add #'mu4e-display-buffer :around #'prot/mu4e)
(defun prot/mu4e-no-header-line ()
(setq-local header-line-format nil))
(add-hook 'mu4e-headers-mode-hook #'prot/mu4e-no-header-line)
(prot-emacs-keybind global-map
"C-c m" #'mu4e
"C-x m" #'mu4e-compose-new) ; override `compose-mail'
(prot-emacs-keybind mu4e-headers-mode-map
"!" #'mu4e-headers-mark-for-flag
"?" #'mu4e-headers-mark-for-unflag
"r" #'mu4e-headers-mark-for-read
"d" #'mu4e-headers-mark-for-delete ; I do not use the trash
"u" #'mu4e-headers-mark-for-unread
"m" #'mu4e-headers-mark-for-unmark
"M" #'mu4e-mark-unmark-all)
(prot-emacs-keybind mu4e-headers-mode-map
"!" #'mu4e-headers-mark-for-flag
"?" #'mu4e-headers-mark-for-unflag
"r" #'mu4e-headers-mark-for-read
"u" #'mu4e-headers-mark-for-unread
"m" #'mu4e-headers-mark-for-unmark
"M" #'mu4e-mark-unmark-all)
(prot-emacs-keybind mu4e-view-mode-map
;; NOTE 2024-02-26: with mu 1.12.0 there is a "wide" reply that I
;; would bind to r and the regular reply to R.
"r" #'mu4e-compose-reply
"w" #'mu4e-copy-thing-at-point
"s" #'mu4e-view-save-attachments
"S" #'mu4e-view-raw-message ; "source" mnemonic
"u" #'mu4e-view-mark-for-unread
"U" #'mu4e-view-mark-for-read
"d" #'mu4e-view-mark-for-delete ; overwrite mu4e-view-mark-for-trash
"!" #'mu4e-view-mark-for-flag
"?" #'mu4e-view-mark-for-unflag))
(let ((map mu4e-headers-mode-map))
;; TODO 2021-01-28: Add wrapper for `mu4e-headers-mark-for-unflag'
(define-key map (kbd "!") (lambda (&optional arg)
(interactive "P")
(if arg
(mu4e-headers-mark-for-unflag)
(mu4e-headers-mark-for-flag))))
(define-key map (kbd "r") #'mu4e-headers-mark-for-read)
(define-key map (kbd "u") #'mu4e-headers-mark-for-unread)
(define-key map (kbd "M-u") #'mu4e-headers-mark-for-unmark)
(define-key map (kbd "C-M-u") #'mu4e-mark-unmark-all))
(provide 'prot-emacs-mu4e)

View file

@ -1,51 +1,52 @@
;;; Notmuch (mail indexer and mail user agent (MUA))
;; NOTE 2022-11-15: See prot-emacs-email.el for the general setup.
;; This file contains the coode specific to notmuch and is loaded from
;; the init.el conditionally if the notmuch executable is available.
;;; Notmuch (mail indexer and mail user agent (MUA))
;; I install notmuch from the distro's repos because the CLI program is
;; not dependent on Emacs. Though the package also includes notmuch.el
;; which is what we use here (they are maintained by the same people).
(prot-emacs-configure
(add-to-list 'load-path "/usr/share/emacs/site-lisp/"))
(add-to-list 'load-path "/usr/share/emacs/site-lisp/")
(prot-emacs-package notmuch
(:delay 1)
;;; Account settings
(prot-emacs-configure
(with-eval-after-load 'prot-common
(let ((prv (prot-common-auth-get-field "prv-gandi" :user))
(pub (prot-common-auth-get-field "pub-gandi" :user))
(inf (prot-common-auth-get-field "inf-gandi" :user))
(box (prot-common-auth-get-field "prot-gandi" :user)))
(setq notmuch-identities
(mapcar (lambda (str)
(format "%s <%s>" user-full-name str))
(list prv pub inf box))
notmuch-fcc-dirs `((".*" . "gandi/Sent"))))))
(let ((prv (prot-common-auth-get-field "prv" :user))
(pub (prot-common-auth-get-field "pub" :user))
(inf (prot-common-auth-get-field "inf" :user)))
(setq notmuch-identities
(mapcar (lambda (str)
(format "%s <%s>" user-full-name str))
(list prv pub inf))
notmuch-fcc-dirs
`((,prv . "prv/Sent")
(,inf . "inf/Sent")
(,pub . "pub/Sent"))))
;;;; General UI
(prot-emacs-configure
(setq notmuch-show-logo nil
notmuch-column-control 1.0
notmuch-hello-auto-refresh t
notmuch-hello-recent-searches-max 20
notmuch-hello-thousands-separator ""
notmuch-hello-sections '(notmuch-hello-insert-saved-searches)
notmuch-show-all-tags-list t))
notmuch-show-all-tags-list t)
;;;; Search
(prot-emacs-configure
(setq notmuch-search-oldest-first nil)
(setq notmuch-search-result-format
(setq notmuch-search-oldest-first nil
notmuch-search-result-format
'(("date" . "%12s ")
("count" . "%-7s ")
("authors" . "%-20s ")
("subject" . "%-80s ")
("tags" . "(%s)")))
(setq notmuch-tree-result-format
("tags" . "(%s)"))
notmuch-tree-result-format
'(("date" . "%12s ")
("authors" . "%-20s ")
((("tree" . "%s")
("subject" . "%s"))
. " %-80s ")
("tags" . "(%s)")))
(setq notmuch-search-line-faces
("tags" . "(%s)"))
notmuch-search-line-faces
'(("unread" . notmuch-search-unread-face)
;; ;; NOTE 2022-09-19: I disable this because I add a cosmeic
;; ;; emoji via `notmuch-tag-formats'. This way I do not get
@ -58,18 +59,19 @@
;; it without any face and I was okay with it. The upside of
;; having a face is that you can identify the message even
;; when the window is split and you don't see the tags.
("flag" . italic)))
(setq notmuch-show-empty-saved-searches t)
(setq notmuch-saved-searches
("flag" . italic))
notmuch-show-empty-saved-searches t
notmuch-saved-searches
`(( :name "📥 inbox"
:query "tag:inbox"
:sort-order newest-first
:key ,(kbd "i"))
( :name "💬 all unread (inbox)"
( :name "💬 unread (inbox)"
:query "tag:unread and tag:inbox"
:sort-order newest-first
:key ,(kbd "u"))
( :name "🛠️ unread packages"
;; My packages
( :name "🗂️ unread packages"
:query "tag:unread and tag:package"
:sort-order newest-first
:key ,(kbd "p"))
@ -77,10 +79,21 @@
( :name "🏆 unread coaching"
:query "tag:unread and tag:coach"
:sort-order newest-first
:key ,(kbd "c")))))
:key ,(kbd "c"))
;; Emacs
( :name "🔨 emacs-devel"
:query "(from:emacs-devel@gnu.org or to:emacs-devel@gnu.org) not tag:archived"
:sort-order newest-first
:key ,(kbd "e d"))
( :name "🦄 emacs-orgmode"
:query "(from:emacs-orgmode@gnu.org or to:emacs-orgmode@gnu.org) not tag:archived"
:sort-order newest-first
:key ,(kbd "e o"))
( :name "🐛 emacs-bugs"
:query "'to:\"/*@debbugs.gnu.org*/\"' not tag:archived"
:sort-order newest-first :key ,(kbd "e b"))))
;;;; Tags
(prot-emacs-configure
(setq notmuch-archive-tags nil ; I do not archive email
notmuch-message-replied-tags '("+replied")
notmuch-message-forwarded-tags '("+forwarded")
@ -106,159 +119,118 @@
'(("del" (notmuch-apply-face tag 'notmuch-tag-added)
(concat "💥" tag))
(".*" (notmuch-apply-face tag 'notmuch-tag-added)
(concat "🏷️" tag)))))
(concat "🏷️" tag))))
;;;; Email composition
(prot-emacs-configure
(setq notmuch-mua-compose-in 'current-window)
(setq notmuch-mua-hidden-headers nil)
(setq notmuch-address-command 'internal)
(setq notmuch-address-use-company nil)
(setq notmuch-always-prompt-for-sender t)
(setq notmuch-mua-cite-function 'message-cite-original-without-signature)
(setq notmuch-mua-reply-insert-header-p-function 'notmuch-show-reply-insert-header-p-never)
(setq notmuch-mua-user-agent-function nil)
(setq notmuch-maildir-use-notmuch-insert t)
(setq notmuch-crypto-process-mime t)
(setq notmuch-crypto-get-keys-asynchronously t)
(setq notmuch-mua-attachment-regexp ; see `notmuch-mua-send-hook'
(setq notmuch-mua-compose-in 'current-window
notmuch-mua-hidden-headers nil ; TODO 2021-05-12: Review hidden headers
notmuch-address-command 'internal
notmuch-always-prompt-for-sender t
notmuch-mua-cite-function 'message-cite-original-without-signature
notmuch-mua-reply-insert-header-p-function 'notmuch-show-reply-insert-header-p-never
notmuch-mua-user-agent-function nil
notmuch-maildir-use-notmuch-insert t
notmuch-crypto-process-mime t
notmuch-crypto-get-keys-asynchronously t
notmuch-mua-attachment-regexp ; see `notmuch-mua-send-hook'
(concat "\\b\\(attache\?ment\\|attached\\|attach\\|"
"pi[èe]ce\s+jointe?\\|"
"συνημμ[εέ]νο\\|επισυν[αά]πτω\\)\\b"))
(with-eval-after-load 'message
(defun prot-notmuch-message-tab ()
"Override for `message-tab' to enforce header line check.
More specifically, perform address completion when on a relevant header
line, because `message-tab' sometimes (not sure when/how) fails to do
that and instead tries to complete against dictionary entries."
(interactive nil message-mode)
(cond
((save-excursion
(goto-char (line-beginning-position))
(looking-at notmuch-address-completion-headers-regexp))
(notmuch-address-expand-name)
;; Completion was performed; nothing else to do.
nil)
(message-tab-body-function (funcall message-tab-body-function))
(t (funcall (or (lookup-key text-mode-map "\t")
(lookup-key global-map "\t")
'indent-relative)))))
(advice-add #'message-tab :override #'prot-notmuch-message-tab)))
;;;; Reading messages
(prot-emacs-configure
(setq notmuch-show-relative-dates t)
(setq notmuch-show-all-multipart/alternative-parts nil)
(setq notmuch-show-indent-messages-width 0)
(setq notmuch-show-indent-multipart nil)
(setq notmuch-show-part-button-default-action 'notmuch-show-view-part)
(setq notmuch-show-text/html-blocked-images ".") ; block everything
(setq notmuch-wash-wrap-lines-length 120)
(setq notmuch-unthreaded-show-out nil)
(setq notmuch-message-headers '("To" "Cc" "Subject" "Date"))
(setq notmuch-message-headers-visible t)
(setq notmuch-show-relative-dates t
notmuch-show-all-multipart/alternative-parts nil
notmuch-show-indent-messages-width 0
notmuch-show-indent-multipart nil
notmuch-show-part-button-default-action 'notmuch-show-view-part
notmuch-show-text/html-blocked-images "." ; block everything
notmuch-wash-wrap-lines-length 120
notmuch-unthreaded-show-out nil
notmuch-message-headers '("To" "Cc" "Subject" "Date")
notmuch-message-headers-visible t)
(let ((count most-positive-fixnum)) ; I don't like the buttonisation of long quotes
(setq notmuch-wash-citation-lines-prefix count
notmuch-wash-citation-lines-suffix count)))
notmuch-wash-citation-lines-suffix count))
;;;; Hooks and key bindings
(prot-emacs-configure
(add-hook 'notmuch-mua-send-hook #'notmuch-mua-attachment-check) ; also see `notmuch-mua-attachment-regexp'
(add-hook 'notmuch-show-hook (lambda () (setq-local header-line-format nil)))
(add-hook 'notmuch-mua-send-hook #'notmuch-mua-attachment-check)
(remove-hook 'notmuch-show-hook #'notmuch-show-turn-on-visual-line-mode)
(remove-hook 'notmuch-search-hook #'notmuch-hl-line-mode) ; Check my `lin' package
(add-hook 'notmuch-show-hook (lambda () (setq-local header-line-format nil)))
(prot-emacs-keybind global-map
"C-c m" #'notmuch
"C-x m" #'notmuch-mua-new-mail) ; override `compose-mail'
(with-eval-after-load 'notmuch
(prot-emacs-keybind notmuch-search-mode-map ; I normally don't use the tree view, otherwise check `notmuch-tree-mode-map'
"a" nil ; the default is too easy to hit accidentally and I do not archive stuff
"A" nil
"/" #'notmuch-search-filter ; alias for l
"r" #'notmuch-search-reply-to-thread ; easier to reply to all by default
"R" #'notmuch-search-reply-to-thread-sender)
(prot-emacs-keybind notmuch-show-mode-map
"a" nil ; the default is too easy to hit accidentally and I do not archive stuff
"A" nil
"r" #'notmuch-show-reply ; easier to reply to all by default
"R" #'notmuch-show-reply-sender)
(define-key notmuch-hello-mode-map (kbd "C-<tab>") nil)))
(prot-emacs-keybind notmuch-search-mode-map ; I normally don't use the tree view, otherwise check `notmuch-tree-mode-map'
"/" #'notmuch-search-filter ; alias for l
"r" #'notmuch-search-reply-to-thread ; easier to reply to all by default
"R" #'notmuch-search-reply-to-thread-sender)
(prot-emacs-keybind notmuch-show-mode-map
"r" #'notmuch-show-reply ; easier to reply to all by default
"R" #'notmuch-show-reply-sender)
(define-key notmuch-hello-mode-map (kbd "C-<tab>") nil))
;;; My own tweaks for notmuch (prot-notmuch.el)
(prot-emacs-configure
(with-eval-after-load 'notmuch
(require 'prot-notmuch)
(prot-emacs-package prot-notmuch
(:delay 1)
;; Those are for the actions that are available after pressing 'k'
;; (`notmuch-tag-jump'). For direct actions, refer to the key
;; bindings below.
(setq notmuch-tagging-keys
`((,(kbd "d") prot-notmuch-mark-delete-tags "💥 Mark for deletion")
(,(kbd "f") prot-notmuch-mark-flag-tags "🚩 Flag as important")
(,(kbd "s") prot-notmuch-mark-spam-tags "🔥 Mark as spam")
(,(kbd "r") ("-unread") "👁️‍🗨️ Mark as read")
(,(kbd "u") ("+unread") "🗨️ Mark as unread")))
(prot-emacs-keybind notmuch-search-mode-map
"D" #'prot-notmuch-search-delete-thread
"S" #'prot-notmuch-search-spam-thread
"g" #'prot-notmuch-refresh-buffer)
;; These emoji are purely cosmetic. The tag remains the same: I
;; would not like to input emoji for searching.
(add-to-list 'notmuch-tag-formats '("encrypted" (concat tag "🔒")))
(add-to-list 'notmuch-tag-formats '("attachment" (concat tag "📎")))
(add-to-list 'notmuch-tag-formats '("coach" (concat tag "🏆")))
(add-to-list 'notmuch-tag-formats '("package" (concat tag "🗂️")))
(prot-emacs-keybind notmuch-show-mode-map
"D" #'prot-notmuch-show-delete-message
"S" #'prot-notmuch-show-spam-message)
(dolist (fn '(prot-notmuch-check-valid-sourcehut-email
prot-notmuch-ask-sourcehut-control-code))
(add-hook 'notmuch-mua-send-hook fn))
(define-key notmuch-show-stash-map (kbd "S") #'prot-notmuch-stash-sourcehut-link)
;; Those are for the actions that are available after pressing 'k'
;; (`notmuch-tag-jump'). For direct actions, refer to the key
;; bindings below.
(setq notmuch-tagging-keys
`((,(kbd "d") prot-notmuch-mark-delete-tags "💥 Mark for deletion")
(,(kbd "f") prot-notmuch-mark-flag-tags "🚩 Flag as important")
(,(kbd "s") prot-notmuch-mark-spam-tags "🔥 Mark as spam")
(,(kbd "r") ("-unread") "👁️‍🗨️ Mark as read")
(,(kbd "u") ("+unread") "🗨️ Mark as unread")))
;; These emoji are purely cosmetic. The tag remains the same: I
;; would not like to input emoji for searching.
(add-to-list 'notmuch-tag-formats '("encrypted" (concat tag "🔒")))
(add-to-list 'notmuch-tag-formats '("attachment" (concat tag "📎")))
(add-to-list 'notmuch-tag-formats '("coach" (concat tag "🏆")))
(add-to-list 'notmuch-tag-formats '("package" (concat tag "🗂️")))))
(prot-emacs-keybind notmuch-search-mode-map
"a" nil ; the default is too easy to hit accidentally
"A" nil
"D" #'prot-notmuch-search-delete-thread
"S" #'prot-notmuch-search-spam-thread
"g" #'prot-notmuch-refresh-buffer)
(prot-emacs-keybind notmuch-show-mode-map
"a" nil ; the default is too easy to hit accidentally
"A" nil
"D" #'prot-notmuch-show-delete-message
"S" #'prot-notmuch-show-spam-message)
(define-key notmuch-show-stash-map (kbd "S") #'prot-notmuch-stash-sourcehut-link)
;; Like C-c M-h for `message-insert-headers'
(define-key notmuch-message-mode-map (kbd "C-c M-e") #'prot-notmuch-patch-add-email-control-code))
;;; Glue code for notmuch and org-link (ol-notmuch.el)
(prot-emacs-configure
(prot-emacs-install ol-notmuch)
(with-eval-after-load 'notmuch
(require 'ol-notmuch)))
(prot-emacs-package ol-notmuch (:install t) (:delay 5))
;;; notmuch-indicator (another package of mine)
(prot-emacs-configure
(prot-emacs-install notmuch-indicator)
(with-eval-after-load 'notmuch
(setq notmuch-indicator-args
'(( :terms "tag:unread and tag:inbox"
:label "[U] "
;; :label "💬 "
:label-face prot-modeline-indicator-cyan
:counter-face prot-modeline-indicator-cyan)
( :terms "tag:unread and tag:package"
:label "[P] "
;; :label "🛠️ "
:label-face prot-modeline-indicator-magenta
:counter-face prot-modeline-indicator-magenta)
( :terms "tag:unread and tag:coach"
:label "[C] "
;; :label "🏆 "
:label-face prot-modeline-indicator-red
:counter-face prot-modeline-indicator-red))
(prot-emacs-package notmuch-indicator
(:install t)
(:delay 5)
notmuch-indicator-refresh-count (* 60 3)
notmuch-indicator-hide-empty-counters t
notmuch-indicator-force-refresh-commands '(notmuch-refresh-this-buffer))
(setq notmuch-indicator-args
'((:terms "tag:unread and tag:inbox and not tag:package and not tag:coach" :label "" :face prot-modeline-indicator-blue)
(:terms "tag:unread and tag:package" :label "" :face prot-modeline-indicator-cyan)
(:terms "tag:unread and tag:coach" :label "" :face prot-modeline-indicator-magenta))
;; I control its placement myself. See prot-emacs-modeline.el where
;; I set the `mode-line-format'.
(setq notmuch-indicator-add-to-mode-line-misc-info nil)
notmuch-indicator-refresh-count (* 60 3)
notmuch-indicator-hide-empty-counters t
notmuch-indicator-force-refresh-commands '(notmuch-refresh-this-buffer))
(notmuch-indicator-mode 1)))
;; I control its placement myself. See prot-emacs-modeline.el where
;; I set the `mode-line-format'.
(setq notmuch-indicator-add-to-mode-line-misc-info nil)
(notmuch-indicator-mode 1))
(provide 'prot-emacs-notmuch)

View file

@ -1,521 +1,496 @@
(prot-emacs-configure
(:delay 5)
;;; Calendar
(prot-emacs-configure
(with-eval-after-load 'calendar
(setq calendar-mark-diary-entries-flag nil)
(setq calendar-mark-holidays-flag t)
(setq calendar-mode-line-format nil)
(setq calendar-time-display-form
'( 24-hours ":" minutes
(when time-zone (format "(%s)" time-zone))))
(setq calendar-week-start-day 1) ; Monday
(setq calendar-date-style 'iso)
(setq calendar-time-zone-style 'numeric) ; Emacs 28.1
(setq calendar-mark-diary-entries-flag nil)
(setq calendar-mark-holidays-flag t)
(setq calendar-mode-line-format nil)
(setq calendar-time-display-form
'( 24-hours ":" minutes
(when time-zone (format "(%s)" time-zone))))
(setq calendar-week-start-day 1) ; Monday
(setq calendar-date-style 'iso)
(setq calendar-time-zone-style 'numeric) ; Emacs 28.1
(require 'solar)
(setq calendar-latitude 35.17 ; Not my actual coordinates
calendar-longitude 33.36)
;; (require 'solar)
;; (setq calendar-latitude 35.17 ; Not my actual coordinates
;; calendar-longitude 33.36)
(require 'cal-dst)
(setq calendar-standard-time-zone-name "+0200")
(setq calendar-daylight-time-zone-name "+0300")))
(prot-emacs-configure
(prot-emacs-install-vc
institution-calendar
"https://github.com/protesilaos/institution-calendar.git")
;; If you want to permanently change what M-x calendar shows, enable
;; `institution-calendar-mode'. Otherwise, use the relevant command:
;;
;; `institution-calendar-cambridge-university'
;; `institution-calendar-oxford-university'
;;
;; And so on for newly registered institutions.
;; Remember to read the manual on how to register your own institution!
(setopt institution-calendar-entity 'oxford-university)
(setopt institution-calendar-include-extra-week-numbers t)
(setopt institution-calendar-include-intermonth-header nil))
;; (require 'cal-dst)
(setq calendar-standard-time-zone-name "+0200")
(setq calendar-daylight-time-zone-name "+0300")
;;; Appt (appointment reminders which also integrate with Org agenda)
(prot-emacs-configure
(setq appt-display-diary nil)
(setq appt-display-format nil)
(setq appt-display-mode-line t)
(setq appt-display-interval 3)
(setq appt-audible nil) ; TODO 2023-01-25: t does nothing because I disable `ring-bell-function'?
(setq appt-warning-time-regexp "appt \\([0-9]+\\)") ; This is for the diary
(setq appt-message-warning-time 6)
(setq appt-display-diary nil
appt-display-format nil
appt-display-mode-line t
appt-display-interval 3
appt-audible nil ; TODO 2023-01-25: t does nothing because I disable `ring-bell-function'?
appt-warning-time-regexp "appt \\([0-9]+\\)" ; This is for the diary
appt-message-warning-time 6)
(with-eval-after-load 'org-agenda
(appt-activate 1)
;; NOTE 2021-12-07: In my `prot-org.el' (see further below), I add
;; `org-agenda-to-appt' to various relevant hooks.
;;
;; Create reminders for tasks with a due date when this file is read.
(org-agenda-to-appt)))
(appt-activate 1))
;;; Org-mode (personal information manager)
(prot-emacs-configure
;; NOTE 2023-05-20: Must be evaluated before Org is loaded,
;; otherwise we have to use the Custom UI. No thanks!
(setq org-export-backends '(html texinfo md))
(setq org-directory (expand-file-name "~/Documents/org/"))
(setq org-imenu-depth 7)
(add-to-list 'safe-local-variable-values '(org-hide-leading-stars . t))
(add-to-list 'safe-local-variable-values '(org-hide-macro-markers . t))
(prot-emacs-keybind global-map
"C-c l" #'org-store-link
"C-c o" #'org-open-at-point-global)
(with-eval-after-load 'org
(prot-emacs-keybind org-mode-map
;; I don't like that Org binds one zillion keys, so if I want one
;; for something more important, I disable it from here.
"C-a" nil
"C-d" nil
"C-S-d" nil
"C-'" nil
"C-," nil
"M-;" nil
"<C-return>" nil
"<C-S-return>" nil
"C-M-S-<right>" nil
"C-M-S-<left>" nil
"C-c ;" nil
"C-c C-x C-c" nil ; disable `org-columns', which I never use
"C-c M-l" #'org-insert-last-stored-link
"C-c C-M-l" #'org-toggle-link-display
"M-." #'org-edit-special) ; alias for C-c ' (mnenomic is global M-. that goes to source
(define-key org-src-mode-map (kbd "M-,") #'org-edit-src-exit) ; see M-. above
(prot-emacs-keybind narrow-map
"b" #'org-narrow-to-block
"e" #'org-narrow-to-element
"s" #'org-narrow-to-subtree)
(prot-emacs-keybind ctl-x-x-map
"i" #'prot-org-id-headlines
"r" #'prot-org-id-headlines-readable
"h" #'prot-org-ox-html)
;; My custom extras, which I use for the agenda and a few other Org features.
(require 'prot-org)
;;;; general settings
(setq org-ellipsis "")
(setq org-special-ctrl-a/e nil)
(setq org-special-ctrl-k nil)
(setq org-M-RET-may-split-line '((default . nil)))
(setq org-insert-heading-respect-content t)
(setq org-hide-emphasis-markers nil)
(setq org-hide-macro-markers nil)
(setq org-hide-leading-stars nil)
(setq org-cycle-separator-lines 0)
(setq org-structure-template-alist
'(("s" . "src")
("e" . "src emacs-lisp")
("E" . "src emacs-lisp :results value code :lexical t")
("t" . "src emacs-lisp :tangle FILENAME")
("T" . "src emacs-lisp :tangle FILENAME :mkdirp yes")
("x" . "example")
("X" . "export")
("q" . "quote")))
(setq org-fold-catch-invisible-edits 'show)
(setq org-yank-folded-subtrees nil)
(setq org-return-follows-link t)
(setq org-loop-over-headlines-in-active-region 'start-level)
(setq org-modules '(ol-info ol-eww))
(setq org-use-sub-superscripts '{})
(setq org-read-date-prefer-future 'time)
(setq org-highlight-latex-and-related nil) ; other options affect elisp regexp in src blocks
(setq org-fontify-quote-and-verse-blocks t)
(setq org-fontify-whole-block-delimiter-line t)
(setq org-track-ordered-property-with-tag t)
(setq org-highest-priority ?A)
(setq org-lowest-priority ?C)
(setq org-default-priority ?A)
(setq org-priority-faces nil)
;; See my `pulsar' package, defined elsewhere in this setup.
(prot-emacs-hook
(org-agenda-after-show-hook org-follow-link-hook)
(pulsar-recenter-center pulsar-reveal-entry)
nil
pulsar)))
;;;; `org-indent-mode' and initial folding
(prot-emacs-configure
(add-hook 'org-mode-hook #'org-indent-mode)
(setq org-indent-mode-turns-on-hiding-stars nil)
(setq org-adapt-indentation nil) ; No, non, nein, όχι to literal indentation!
(setq org-indent-indentation-per-level 4)
(setq org-startup-folded 'content))
(setq org-ellipsis "")
(setq org-adapt-indentation nil) ; No, non, nein, όχι!
(setq org-special-ctrl-a/e nil)
(setq org-special-ctrl-k nil)
(setq org-M-RET-may-split-line '((default . nil)))
(setq org-hide-emphasis-markers nil)
(setq org-hide-macro-markers nil)
(setq org-hide-leading-stars nil)
(setq org-cycle-separator-lines 0)
(setq org-structure-template-alist
'(("s" . "src")
("e" . "src emacs-lisp")
("E" . "src emacs-lisp :results value code :lexical t")
("t" . "src emacs-lisp :tangle FILENAME")
("T" . "src emacs-lisp :tangle FILENAME :mkdirp yes")
("x" . "example")
("q" . "quote")))
(setq org-catch-invisible-edits 'show)
(setq org-return-follows-link nil)
(setq org-loop-over-headlines-in-active-region 'start-level)
(setq org-modules '(ol-info ol-eww))
(setq org-use-sub-superscripts '{})
(setq org-insert-heading-respect-content t)
;;;; refile, todo
(with-eval-after-load 'org
(setq org-refile-targets
'((org-agenda-files . (:maxlevel . 2))
(nil . (:maxlevel . 2))))
(setq org-refile-use-outline-path nil)
(setq org-refile-use-outline-path t)
(setq org-refile-allow-creating-parent-nodes 'confirm)
(setq org-refile-use-cache t)
(setq org-reverse-note-order nil)
;; ;; NOTE 2023-04-07: Leaving this here for demo purposes.
;; (setq org-todo-keywords
;; '((sequence "TODO(t)" "MAYBE(m)" "WAIT(w@/!)" "|" "CANCEL(c@)" "DONE(d!)")
;; (sequence "COACH(k)" "|" "COACHED(K!)")))
(setq org-todo-keywords
'((sequence "TODO(t)" "MAYBE(m)" "|" "CANCELLED(c@)" "DONE(d!)")))
'((sequence "TODO(t)" "|" "CANCEL(c@)" "DONE(d!)")
(sequence "COACH(k)" "|" "COACHED(K!)")))
(defface prot/org-todo-alternative
'((t :inherit (italic org-todo)))
"Face for alternative TODO-type Org keywords.")
(defface prot/org-done-alternative
'((t :inherit (italic org-done)))
"Face for alternative DONE-type Org keywords.")
(defface prot/org-bold-done
'((t :inherit (bold org-done)))
"Face for bold DONE-type Org keywords.")
(setq org-todo-keyword-faces
'(("MAYBE" . prot/org-todo-alternative)
("CANCELLED" . prot/org-done-alternative)))
(defface prot/org-tag-coaching
'((default :inherit unspecified :weight regular :slant normal)
(((class color) (min-colors 88) (background light))
:foreground "#004476")
(((class color) (min-colors 88) (background dark))
:foreground "#c0d0ef")
(t :foreground "cyan"))
"Face for coaching Org tag.")
(defface prot/org-tag-protasks
'((default :inherit unspecified :weight regular :slant normal)
(((class color) (min-colors 88) (background light))
:foreground "#603f00")
(((class color) (min-colors 88) (background dark))
:foreground "#deba66")
(t :foreground "yellow"))
"Face for protasks Org tag.")
(when (eq prot-emacs-load-theme-family 'modus)
(setq org-tag-faces
'(("coaching" . prot/org-tag-coaching)
("protasks" . prot/org-tag-protasks))))
'(("CANCEL" . prot/org-bold-done)))
(setq org-use-fast-todo-selection 'expert)
(setq org-priority-faces nil)
(setq org-fontify-done-headline nil)
(setq org-fontify-todo-headline nil)
(setq org-fontify-quote-and-verse-blocks t)
(setq org-fontify-whole-heading-line nil)
(setq org-fontify-whole-block-delimiter-line t)
(setq org-highlight-latex-and-related nil) ; other options affect elisp regexp in src blocks
(setq org-enforce-todo-dependencies t)
(setq org-enforce-todo-checkbox-dependencies t))
(setq org-enforce-todo-checkbox-dependencies t)
(setq org-track-ordered-property-with-tag t)
(setq org-highest-priority ?A)
(setq org-lowest-priority ?C)
(setq org-default-priority ?A)
;;;; tags
(prot-emacs-configure
(with-eval-after-load 'org
(setq org-tag-alist nil)
(setq org-auto-align-tags nil)
(setq org-tags-column 0)))
(setq org-tag-alist ; I don't really use those, but whatever
'(("meeting")
("admin")
("emacs")
("modus")
("politics")
("economics")
("philosophy")
("book")
("essay")
("mail")
("purchase")
("hardware")
("software")
("website")))
(setq org-auto-align-tags nil)
(setq org-tags-column 0)
;;;; log
(prot-emacs-configure
(with-eval-after-load 'org
(setq org-log-done 'time)
(setq org-log-into-drawer t)
(setq org-log-note-clock-out nil)
(setq org-log-redeadline 'time)
(setq org-log-reschedule 'time)))
(setq org-log-done 'time)
(setq org-log-into-drawer t)
(setq org-log-note-clock-out nil)
(setq org-log-redeadline 'time)
(setq org-log-reschedule 'time)
(setq org-read-date-prefer-future 'time)
;;;; links
(prot-emacs-configure
(with-eval-after-load 'org
(setq org-return-follows-link t)
(setq org-link-context-for-files t)
(setq org-link-keep-stored-after-insertion nil)
(setq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)))
;;;; code blocks
(prot-emacs-configure
(with-eval-after-load 'org
(setq org-confirm-babel-evaluate nil)
(setq org-src-window-setup 'current-window)
(setq org-edit-src-persistent-message nil)
(setq org-src-fontify-natively t)
(setq org-src-preserve-indentation t)
(setq org-src-tab-acts-natively t)
(setq org-edit-src-content-indentation 0)))
;;;; export
(prot-emacs-configure
;; NOTE 2023-05-20: Must be evaluated before Org is loaded,
;; otherwise we have to use the Custom UI. No thanks!
(setq org-export-backends '(html texinfo md))
(with-eval-after-load 'org
(setq org-export-with-toc t)
(setq org-export-headline-levels 8)
(setq org-export-dispatch-use-expert-ui nil)
(setq org-html-htmlize-output-type nil)
(setq org-html-head-include-default-style nil)
(setq org-html-head-include-scripts nil)))
(setq org-link-keep-stored-after-insertion nil)
;; TODO 2021-10-15 org-link-make-description-function
;;;; capture
(prot-emacs-configure
(define-key global-map (kbd "C-c c") #'org-capture)
(setq org-capture-templates
`(("b" "Basic task for future review" entry
(file+headline "tasks.org" "Tasks to be reviewed")
,(concat "* %^{Title}\n"
":PROPERTIES:\n"
":CAPTURED: %U\n"
":END:\n\n"
"%i %l")
:empty-lines-after 1)
("c" "Clock in to a task" entry
(file+headline "tasks.org" "Clocked tasks")
,(concat "* TODO %^{Title}\n"
":PROPERTIES:\n"
":EFFORT: %^{Effort estimate in minutes|5|10|15|30|45|60|90|120}\n"
":END:\n\n"
"%a\n")
:prepend t
:clock-in t
:clock-keep t
:immediate-finish t
:empty-lines-after 1)
("m" "Memorandum of conversation" entry
(file+headline "tasks.org" "Tasks to be reviewed")
,(concat "* Memorandum of conversation with %^{Person}\n"
":PROPERTIES:\n"
":CAPTURED: %U\n"
":END:\n\n"
"%i%?")
:empty-lines-after 1)
("t" "Task with a due date" entry
(file+headline "tasks.org" "Tasks with a date")
,(concat "* TODO %^{Title} %^g\n"
"SCHEDULED: %^t\n"
":PROPERTIES:\n"
":CAPTURED: %U\n"
":END:\n\n"
"%a\n%i%?")
:empty-lines-after 1)
("e" "Email note" entry
(file+headline "tasks.org" "Tasks to be reviewed")
,(concat "* TODO %:subject :mail:\n"
":PROPERTIES:\n"
":CAPTURED: %U\n"
":END:\n\n"
"%a\n%i%?")
:empty-lines-after 1)
;; ;; NOTE 2023-01-29: See improved version in the
;; ;; prot-org.el further below. It runs a custom function
;; ;; of mine that produces a more personalised template
;; ;; than what the built-in options support.
;;
;; ("p" "Private lesson or service" entry
;; (file "coach.org")
;; ,(concat "* COACH %^{Title} %^g\n"
;; "%(prot/org-date-prompt-range-increment)"
;; ":PROPERTIES:\n"
;; ":CAPTURED: %U\n"
;; ":APPT_WARNTIME: 20\n"
;; ":END:\n\n"
;; "%a\n%i%?")
;; :prepend t
;; :empty-lines 1)
))
(with-eval-after-load 'org-capture
(require 'prot-org)
(setq org-capture-templates
(let* ((without-time (concat ":PROPERTIES:\n"
":CAPTURED: %U\n"
":CUSTOM_ID: h:%(format-time-string \"%Y%m%dT%H%M%S\")\n"
":END:\n\n"
"%a\n%?"))
(with-time (concat "DEADLINE: %^T\n"
":PROPERTIES:\n"
":CAPTURED: %U\n"
":CUSTOM_ID: h:%(format-time-string \"%Y%m%dT%H%M%S\")\n"
":APPT_WARNTIME: 20\n"
":END:\n\n"
"%a%?")))
`(("s" "Select file and heading to add to" entry
(function prot-org-select-heading-in-file)
,(concat "* TODO %^{Title}%?\n" without-time)
:empty-lines-after 1)
("t" "Task to do" entry
(file+headline "tasks.org" "All tasks")
,(concat "* TODO %^{Title} %^g\n" without-time)
:empty-lines-after 1)
("w" "Wishlist" entry
(file+olp "tasks.org" "All tasks" "Wishlist")
,(concat "* %^{Title} %^g\n" without-time)
:empty-lines-after 1)
("·" "") ; HACK to get a divider
("a" "Appointment" entry
(file+headline "tasks.org" "Appointments")
,(concat "* TODO %^{Title}\n" with-time)
:empty-lines-after 1)
("c" "Coaching appointment" entry
(file "coach.org")
,(concat "* TODO %^{Person and description}\n" with-time)
:prepend t
:empty-lines 1)
("p" "Prot Asks" entry
(file+headline "tasks.org" "Prot Asks")
,(concat "* TODO %^{Title}\n" with-time)
:empty-lines-after 1)
("v" "Video call" entry
(file+headline "tasks.org" "Video calls")
,(concat "* TODO %^{Title}\n" with-time)
:empty-lines-after 1))))))
(setq org-capture-templates-contexts
'(("e" ((in-mode . "notmuch-search-mode")
(in-mode . "notmuch-show-mode")
(in-mode . "notmuch-tree-mode")))))
;;;; agenda
(prot-emacs-configure
;; I bind `org-agenda' to C-c A, so this one puts me straight into my
;; custom block agenda.
(prot-emacs-keybind global-map
"C-c A" #'org-agenda
"C-c a" (lambda ()
"Call Org agenda with `prot-org-custom-daily-agenda' configuration."
(interactive)
(org-agenda nil "A")))
(with-eval-after-load 'org-agenda
(prot-emacs-keybind org-agenda-mode-map
"n" #'org-agenda-next-item
"p" #'org-agenda-previous-item)
;;;;; Custom agenda blocks
(require 'prot-org)
(setq org-agenda-format-date #'prot-org-agenda-format-date-aligned)
;; Check the variable `prot-org-custom-daily-agenda' in prot-org.el
(setq org-agenda-custom-commands
`(("A" "Daily agenda and top priority tasks"
,prot-org-custom-daily-agenda
((org-agenda-fontify-priorities nil)
(org-agenda-dim-blocked-tasks nil)))
("P" "Plain text daily agenda and top priorities"
,prot-org-custom-daily-agenda
((org-agenda-with-colors nil)
(org-agenda-prefix-format "%t %s")
(org-agenda-current-time-string ,(car (last org-agenda-time-grid)))
(org-agenda-fontify-priorities nil)
(org-agenda-remove-tags t))
("agenda.txt"))
("p" "Prot Asks"
,prot-org-custom-prot-asks-agenda)))
;;;;; Basic agenda setup
(setq org-default-notes-file (make-temp-file "emacs-org-notes-")) ; send it to oblivion
(setq org-agenda-files (list org-directory))
(setq org-agenda-span 'week)
(setq org-agenda-start-on-weekday 1) ; Monday
(setq org-agenda-confirm-kill t)
(setq org-agenda-show-all-dates t)
(setq org-agenda-show-outline-path nil)
(setq org-agenda-window-setup 'current-window)
(setq org-agenda-skip-comment-trees t)
(setq org-agenda-menu-show-matcher t)
(setq org-agenda-menu-two-columns nil)
(setq org-agenda-sticky nil)
(setq org-agenda-custom-commands-contexts nil)
(setq org-agenda-max-entries nil)
(setq org-agenda-max-todos nil)
(setq org-agenda-max-tags nil)
(setq org-agenda-max-effort nil)
(setq org-default-notes-file (make-temp-file "emacs-org-notes-")) ; send it to oblivion
(setq org-agenda-files `(,org-directory))
(setq org-agenda-span 'week)
(setq org-agenda-start-on-weekday 1) ; Monday
(setq org-agenda-confirm-kill t)
(setq org-agenda-show-all-dates t)
(setq org-agenda-show-outline-path nil)
(setq org-agenda-window-setup 'current-window)
(setq org-agenda-skip-comment-trees t)
(setq org-agenda-menu-show-matcher t)
(setq org-agenda-menu-two-columns nil)
(setq org-agenda-sticky nil)
(setq org-agenda-custom-commands-contexts nil)
(setq org-agenda-max-entries nil)
(setq org-agenda-max-todos nil)
(setq org-agenda-max-tags nil)
(setq org-agenda-max-effort nil)
;; NOTE 2021-12-07: In my `prot-org.el' (see further below), I add
;; `org-agenda-to-appt' to various relevant hooks.
;;
;; Create reminders for tasks with a due date when this file is read.
(run-at-time (* 60 5) nil #'org-agenda-to-appt)
;;;;; General agenda view options
;; ;; NOTE 2021-12-07: Also see my `org-agenda-custom-commands'
;; (setq org-agenda-prefix-format
;; '((agenda . " %i %-12:c%?-12t% s")
;; (todo . " %i %-12:c")
;; (tags . " %i %-12:c")
;; (search . " %i %-12:c")))
(setq org-agenda-prefix-format "%c %t %s")
(setq org-agenda-sorting-strategy
'(((agenda habit-down time-up priority-down category-keep)
(todo priority-down category-keep)
(tags priority-down category-keep)
(search category-keep))))
(setq org-agenda-breadcrumbs-separator "->")
(setq org-agenda-todo-keyword-format "%-1s")
(setq org-agenda-fontify-priorities 'cookies)
(setq org-agenda-category-icon-alist nil)
(setq org-agenda-remove-times-when-in-prefix nil)
(setq org-agenda-remove-timeranges-from-blocks nil)
(setq org-agenda-compact-blocks nil)
(setq org-agenda-block-separator ?—)
;; NOTE 2021-12-07: Check further below my `org-agenda-custom-commands'
(setq org-agenda-prefix-format
'((agenda . " %i %-12:c%?-12t% s")
(todo . " %i %-12:c")
(tags . " %i %-12:c")
(search . " %i %-12:c")))
(setq org-agenda-sorting-strategy
'(((agenda habit-down time-up priority-down category-keep)
(todo priority-down category-keep)
(tags priority-down category-keep)
(search category-keep))))
(setq org-agenda-breadcrumbs-separator "->")
(setq org-agenda-todo-keyword-format "%-1s")
(setq org-agenda-fontify-priorities 'cookies)
(setq org-agenda-category-icon-alist nil)
(setq org-agenda-remove-times-when-in-prefix nil)
(setq org-agenda-remove-timeranges-from-blocks nil)
(setq org-agenda-compact-blocks nil)
(setq org-agenda-block-separator ?—)
;;;;; Agenda marks
(setq org-agenda-bulk-mark-char "#")
(setq org-agenda-persistent-marks nil)
(setq org-agenda-bulk-mark-char "#")
(setq org-agenda-persistent-marks nil)
;;;;; Agenda diary entries
(setq org-agenda-insert-diary-strategy 'date-tree)
(setq org-agenda-insert-diary-extract-time nil)
(setq org-agenda-include-diary nil)
;; I do not want the diary, but there is no way to disable it
;; altogether. This creates a diary file in the /tmp directory.
(setq diary-file (make-temp-file "emacs-diary-"))
(setq org-agenda-diary-file 'diary-file) ; TODO 2023-05-20: review Org diary substitute
(setq org-agenda-insert-diary-strategy 'date-tree)
(setq org-agenda-insert-diary-extract-time nil)
(setq org-agenda-include-diary nil)
;; I do not want the diary, but there is no way to disable it
;; altogether. This creates a diary file in the /tmp directory.
(setq diary-file (make-temp-file "emacs-diary-"))
(setq org-agenda-diary-file 'diary-file) ; TODO 2023-05-20: review Org diary substitute
;;;;; Agenda follow mode
(setq org-agenda-start-with-follow-mode nil)
(setq org-agenda-follow-indirect t)
(setq org-agenda-start-with-follow-mode nil)
(setq org-agenda-follow-indirect t)
;;;;; Agenda multi-item tasks
(setq org-agenda-dim-blocked-tasks t)
(setq org-agenda-todo-list-sublevels t)
(setq org-agenda-dim-blocked-tasks t)
(setq org-agenda-todo-list-sublevels t)
;;;;; Agenda filters and restricted views
(setq org-agenda-persistent-filter nil)
(setq org-agenda-restriction-lock-highlight-subtree t)
(setq org-agenda-persistent-filter nil)
(setq org-agenda-restriction-lock-highlight-subtree t)
;;;;; Agenda items with deadline and scheduled timestamps
(setq org-agenda-include-deadlines t)
(setq org-deadline-warning-days 0)
(setq org-agenda-skip-scheduled-if-done nil)
(setq org-agenda-skip-scheduled-if-deadline-is-shown t)
(setq org-agenda-skip-timestamp-if-deadline-is-shown t)
(setq org-agenda-skip-deadline-if-done nil)
(setq org-agenda-skip-deadline-prewarning-if-scheduled 1)
(setq org-agenda-skip-scheduled-delay-if-deadline nil)
(setq org-agenda-skip-additional-timestamps-same-entry nil)
(setq org-agenda-skip-timestamp-if-done nil)
(setq org-agenda-search-headline-for-time nil)
(setq org-scheduled-past-days 365)
(setq org-deadline-past-days 365)
(setq org-agenda-move-date-from-past-immediately-to-today t)
(setq org-agenda-show-future-repeats t)
(setq org-agenda-prefer-last-repeat nil)
(setq org-agenda-timerange-leaders
'("" "(%d/%d): "))
(setq org-agenda-scheduled-leaders
'("Scheduled: " "Sched.%2dx: "))
(setq org-agenda-inactive-leader "[")
(setq org-agenda-deadline-leaders
'("Deadline: " "In %3d d.: " "%2d d. ago: "))
;; Time grid
(setq org-agenda-time-leading-zero t)
(setq org-agenda-timegrid-use-ampm nil)
(setq org-agenda-use-time-grid t)
(setq org-agenda-show-current-time-in-grid t)
(setq org-agenda-current-time-string (concat "Now " (make-string 70 ?.)))
(setq org-agenda-time-grid
'((daily today require-timed)
( 0600 0700 0800 0900 1000 1100
1200 1300 1400 1500 1600 1700
1800 1900 2000 2100 2200 2300)
"" ""))
(setq org-agenda-default-appointment-duration nil)
(setq org-agenda-include-deadlines t)
(setq org-deadline-warning-days 0)
(setq org-agenda-skip-scheduled-if-done nil)
(setq org-agenda-skip-scheduled-if-deadline-is-shown t)
(setq org-agenda-skip-timestamp-if-deadline-is-shown t)
(setq org-agenda-skip-deadline-if-done nil)
(setq org-agenda-skip-deadline-prewarning-if-scheduled 1)
(setq org-agenda-skip-scheduled-delay-if-deadline nil)
(setq org-agenda-skip-additional-timestamps-same-entry nil)
(setq org-agenda-skip-timestamp-if-done nil)
(setq org-agenda-search-headline-for-time nil)
(setq org-scheduled-past-days 365)
(setq org-deadline-past-days 365)
(setq org-agenda-move-date-from-past-immediately-to-today t)
(setq org-agenda-show-future-repeats t)
(setq org-agenda-prefer-last-repeat nil)
(setq org-agenda-timerange-leaders
'("" "(%d/%d): "))
(setq org-agenda-scheduled-leaders
'("Scheduled: " "Sched.%2dx: "))
(setq org-agenda-inactive-leader "[")
(setq org-agenda-deadline-leaders
'("Deadline: " "In %3d d.: " "%2d d. ago: "))
;; Time grid
(setq org-agenda-time-leading-zero t)
(setq org-agenda-timegrid-use-ampm nil)
(setq org-agenda-use-time-grid t)
(setq org-agenda-show-current-time-in-grid t)
(setq org-agenda-current-time-string
(concat "Now " (make-string 70 ?-)))
(setq org-agenda-time-grid
'((daily today require-timed)
( 0500 0600 0700 0800 0900 1000
1100 1200 1300 1400 1500 1600
1700 1800 1900 2000 2100 2200)
" ....." "-----------------"))
(setq org-agenda-default-appointment-duration nil)
;;;;; Agenda global to-do list
(setq org-agenda-todo-ignore-with-date t)
(setq org-agenda-todo-ignore-timestamp t)
(setq org-agenda-todo-ignore-scheduled t)
(setq org-agenda-todo-ignore-deadlines t)
(setq org-agenda-todo-ignore-time-comparison-use-seconds t)
(setq org-agenda-tags-todo-honor-ignore-options nil)
(setq org-agenda-todo-ignore-with-date t)
(setq org-agenda-todo-ignore-timestamp t)
(setq org-agenda-todo-ignore-scheduled t)
(setq org-agenda-todo-ignore-deadlines t)
(setq org-agenda-todo-ignore-time-comparison-use-seconds t)
(setq org-agenda-tags-todo-honor-ignore-options nil)
;;;;; Agenda tagged items
(setq org-agenda-show-inherited-tags t)
(setq org-agenda-use-tag-inheritance
'(todo search agenda))
(setq org-agenda-hide-tags-regexp nil)
(setq org-agenda-remove-tags nil)
(setq org-agenda-tags-column 1)
(setq org-agenda-show-inherited-tags t)
(setq org-agenda-use-tag-inheritance
'(todo search agenda))
(setq org-agenda-hide-tags-regexp nil)
(setq org-agenda-remove-tags nil)
(setq org-agenda-tags-column -100)
;;;;; Agenda entry
;; NOTE: I do not use this right now. Leaving everything to its
;; default value.
(setq org-agenda-start-with-entry-text-mode nil)
(setq org-agenda-entry-text-maxlines 5)
(setq org-agenda-entry-text-exclude-regexps nil)
(setq org-agenda-entry-text-leaders " > ")
;; NOTE: I do not use this right now. Leaving everything to its
;; default value.
(setq org-agenda-start-with-entry-text-mode nil)
(setq org-agenda-entry-text-maxlines 5)
(setq org-agenda-entry-text-exclude-regexps nil)
(setq org-agenda-entry-text-leaders " > ")
;;;;; Agenda logging and clocking
;; NOTE: I do not use these yet, though I plan to. Leaving everything
;; to its default value for the time being.
(setq org-agenda-log-mode-items '(closed clock))
(setq org-agenda-clock-consistency-checks
'((:max-duration "10:00" :min-duration 0 :max-gap "0:05" :gap-ok-around
("4:00")
:default-face ; This should definitely be reviewed
((:background "DarkRed")
(:foreground "white"))
:overlap-face nil :gap-face nil :no-end-time-face nil
:long-face nil :short-face nil)))
(setq org-agenda-log-mode-add-notes t)
(setq org-agenda-start-with-log-mode nil)
(setq org-agenda-start-with-clockreport-mode nil)
(setq org-agenda-clockreport-parameter-plist '(:link t :maxlevel 2))
(setq org-agenda-search-view-always-boolean nil)
(setq org-agenda-search-view-force-full-words nil)
(setq org-agenda-search-view-max-outline-level 0)
(setq org-agenda-search-headline-for-time t)
(setq org-agenda-use-time-grid t)
(setq org-agenda-cmp-user-defined nil)
(setq org-agenda-sort-notime-is-late t) ; Org 9.4
(setq org-agenda-sort-noeffort-is-high t) ; Org 9.4
;; ;;;;; Agenda habits
;; (require 'org-habit)
;; (setq org-habit-graph-column 50)
;; (setq org-habit-preceding-days 9)
;; ;; Always show the habit graph, even if there are no habits for
;; ;; today.
;; (setq org-habit-show-all-today t)
;; NOTE: I do not use these yet, though I plan to. Leaving everything
;; to its default value for the time being.
(setq org-agenda-log-mode-items '(closed clock))
(setq org-agenda-clock-consistency-checks
'((:max-duration "10:00" :min-duration 0 :max-gap "0:05" :gap-ok-around
("4:00")
:default-face ; This should definitely be reviewed
((:background "DarkRed")
(:foreground "white"))
:overlap-face nil :gap-face nil :no-end-time-face nil
:long-face nil :short-face nil)))
(setq org-agenda-log-mode-add-notes t)
(setq org-agenda-start-with-log-mode nil)
(setq org-agenda-start-with-clockreport-mode nil)
(setq org-agenda-clockreport-parameter-plist '(:link t :maxlevel 2))
(setq org-agenda-search-view-always-boolean nil)
(setq org-agenda-search-view-force-full-words nil)
(setq org-agenda-search-view-max-outline-level 0)
(setq org-agenda-search-headline-for-time t)
(setq org-agenda-use-time-grid t)
(setq org-agenda-cmp-user-defined nil)
(setq org-agenda-sort-notime-is-late t) ; Org 9.4
(setq org-agenda-sort-noeffort-is-high t) ; Org 9.4
;;;;; Agenda column view
;; NOTE I do not use these, but may need them in the future.
(setq org-agenda-view-columns-initially nil)
(setq org-agenda-columns-show-summaries t)
(setq org-agenda-columns-compute-summary-properties t)
(setq org-agenda-columns-add-appointments-to-effort-sum nil)
(setq org-agenda-auto-exclude-function nil)
(setq org-agenda-bulk-custom-functions nil)))
;; NOTE I do not use these, but may need them in the future.
(setq org-agenda-view-columns-initially nil)
(setq org-agenda-columns-show-summaries t)
(setq org-agenda-columns-compute-summary-properties t)
(setq org-agenda-columns-add-appointments-to-effort-sum nil)
(setq org-agenda-auto-exclude-function nil)
(setq org-agenda-bulk-custom-functions nil)
;;;;; Agenda habits
;; (require 'org-habit)
;; (setq org-habit-graph-column 50)
;; (setq org-habit-preceding-days 9)
;; ;; Always show the habit graph, even if there are no habits for
;; ;; today.
;; (setq org-habit-show-all-today t)
;;;; code blocks
(setq org-confirm-babel-evaluate nil)
(setq org-src-window-setup 'current-window)
(setq org-edit-src-persistent-message nil)
(setq org-src-fontify-natively t)
(setq org-src-preserve-indentation t)
(setq org-src-tab-acts-natively t)
(setq org-edit-src-content-indentation 0)
;;;; export
(setq org-export-with-toc t)
(setq org-export-headline-levels 8)
(setq org-export-dispatch-use-expert-ui nil)
(setq org-html-htmlize-output-type nil)
(setq org-html-head-include-default-style nil)
(setq org-html-head-include-scripts nil)
;; (require 'ox-texinfo)
;; (require 'ox-md)
;;;; IDs
(setq org-id-link-to-org-use-id
'create-if-interactive-and-no-custom-id)
;;;; Hooks and key bindings
;; See my `pulsar' package, defined elsewhere in this setup.
(with-eval-after-load 'pulsar
(dolist (hook '(org-agenda-after-show-hook org-follow-link-hook))
(add-hook hook #'pulsar-recenter-center)
(add-hook hook #'pulsar-reveal-entry)))
(prot-emacs-keybind global-map
"C-c A" #'org-agenda ; see the `prot-org' section for C-c a
"C-c c" #'org-capture
"C-c l" #'org-store-link
"C-c o" #'org-open-at-point-global)
(require 'org)
(prot-emacs-keybind org-mode-map
;; I don't like that Org binds one zillion keys, so if I want one
;; for something more important, I disable it from here.
"C-'" nil
"C-," nil
"M-;" nil
"<C-return>" nil
"<C-S-return>" nil
"C-M-S-<right>" nil
"C-M-S-<left>" nil
"C-c M-l" #'org-insert-last-stored-link
"C-c C-M-l" #'org-toggle-link-display)
(prot-emacs-keybind narrow-map
"b" #'org-narrow-to-block
"e" #'org-narrow-to-element
"s" #'org-narrow-to-subtree)
;;; Custom extensions (prot-org.el)
(require 'prot-org)
(setq org-agenda-format-date #'prot-org-agenda-format-date-aligned)
;; Check the variable `prot-org-custom-daily-agenda' in prot-org.el
(setq org-agenda-custom-commands
`(("A" "Daily agenda and top priority tasks"
,prot-org-custom-daily-agenda
((org-agenda-fontify-priorities nil)
(org-agenda-dim-blocked-tasks nil)))
("P" "Plain text daily agenda and top priorities"
,prot-org-custom-daily-agenda
((org-agenda-with-colors nil)
(org-agenda-prefix-format "%t %s")
(org-agenda-current-time-string ,(car (last org-agenda-time-grid)))
(org-agenda-fontify-priorities nil)
(org-agenda-remove-tags t))
("agenda.txt"))))
(defun prot/org-agenda ()
"Call Org agenda with `prot-org-custom-daily-agenda' configuration."
(interactive)
(org-agenda nil "A"))
;; I bind `org-agenda' to C-c A, so this one puts me straight into my
;; custom block agenda.
(define-key global-map (kbd "C-c a") #'prot/org-agenda)
(prot-emacs-keybind ctl-x-x-map
"i" #'prot-org-id-headlines
"h" #'prot-org-ox-html)
(add-to-list 'org-capture-templates
'("p" "Private lesson or service" entry
(file "coach.org")
#'prot-org-capture-coach
:prepend t
:empty-lines 1))
(add-to-list 'org-capture-templates
'("P" "Private service clocked" entry
(file+headline "coach.org" "Clocked services")
#'prot-org-capture-coach-clock
:prepend t
:clock-in t
:clock-keep t
:immediate-finish t
:empty-lines 1)))
(prot-emacs-package prot-coach
(:delay 10)
(define-key global-map (kbd "C-c j") #'prot-coach-jitsi-link))
(provide 'prot-emacs-org)

View file

@ -1,122 +1,92 @@
;;; Isearch, occur, grep, and extras (prot-search.el)
(prot-emacs-configure
(setq search-whitespace-regexp ".*?")
(setq isearch-lax-whitespace t)
(setq isearch-regexp-lax-whitespace nil))
(prot-emacs-configure
(prot-emacs-package isearch
(:delay 5)
(setq search-highlight t)
(setq search-whitespace-regexp ".*?" ; one `setq' here to make it obvious they are a bundle
isearch-lax-whitespace t
isearch-regexp-lax-whitespace nil)
(setq isearch-lazy-highlight t)
(setq lazy-highlight-initial-delay 0.5)
(setq lazy-highlight-no-delay-length 4))
(prot-emacs-configure
(setq list-matching-lines-jump-to-current-line nil) ; for `occur'
;; All of the following variables were introduced in Emacs 27.1.
(setq isearch-lazy-count t)
(setq lazy-count-prefix-format "(%s/%s) ")
(setq lazy-count-suffix-format nil))
(prot-emacs-configure
(setq lazy-count-suffix-format nil)
(setq isearch-yank-on-move 'shift)
(setq isearch-allow-scroll 'unlimited)
;; These variables are from Emacs 28
(setq isearch-repeat-on-direction-change t)
(setq lazy-highlight-initial-delay 0.5)
(setq lazy-highlight-no-delay-length 3)
(setq isearch-wrap-pause t) ; `no-ding' makes keyboard macros never quit
(setq isearch-repeat-on-direction-change t))
(prot-emacs-configure
(setq query-replace-from-to-separator " > ")
(defun prot/query-replace-preview (match replacement)
"Do the work of `query-replace-show-preview'."
(format "%s%s%s"
(propertize match 'face 'query-replace-preview-match)
(propertize query-replace-from-to-separator 'face 'secondary-selection)
(propertize replacement 'face 'query-replace-preview)))
(setq query-replace-show-preview #'prot/query-replace-preview)) ; Emacs 32
(add-hook 'occur-mode-hook #'hl-line-mode)
(add-hook 'occur-mode-hook #'prot-common-truncate-lines-silently) ; from `prot-common.el'
(prot-emacs-configure
(setq list-matching-lines-jump-to-current-line nil) ; do not jump to current line in `*occur*' buffers
(prot-emacs-hook occur-mode-hook (prot-common-truncate-lines-silently hl-line-mode)))
(define-key minibuffer-local-isearch-map (kbd "M-/") #'isearch-complete-edit)
(define-key occur-mode-map (kbd "t") #'toggle-truncate-lines)
(prot-emacs-keybind isearch-mode-map
"C-g" #'isearch-cancel ; instead of `isearch-abort'
"M-/" #'isearch-complete))
(prot-emacs-configure
(define-key global-map (kbd "C-.") #'isearch-forward-symbol-at-point) ; easier than M-s . // I also have `prot-simple-mark-sexp' on C-,
(define-key minibuffer-local-isearch-map (kbd "M-/") #'isearch-complete-edit)
(define-key occur-mode-map (kbd "t") #'toggle-truncate-lines)
(prot-emacs-keybind isearch-mode-map
"C-g" #'isearch-cancel ; instead of `isearch-abort'
"M-/" #'isearch-complete))
(prot-emacs-package prot-search
(:delay 5)
(setq prot-search-outline-regexp-alist
'((emacs-lisp-mode . "^\\((\\|;;;+ \\)")
(org-mode . "^\\(\\*+ +\\|#\\+[Tt][Ii][Tt][Ll][Ee]:\\)")
(outline-mode . "^\\*+ +")
(emacs-news-view-mode . "^\\*+ +")
(conf-toml-mode . "^\\[")
(markdown-mode . "^#+ +")))
(setq prot-search-todo-keywords
(concat "TODO\\|FIXME\\|NOTE\\|REVIEW\\|XXX\\|KLUDGE"
"\\|HACK\\|WARN\\|WARNING\\|DEPRECATED\\|BUG"))
(prot-emacs-configure
(require 'prot-search)
(prot-emacs-keybind global-map
"M-s M-%" #'prot-search-replace-markup ; see `prot-search-markup-replacements'
"M-s g" #'prot-search-grep
"M-s u" #'prot-search-occur-urls
"M-s t" #'prot-search-occur-todo-keywords
"M-s M-t" #'prot-search-grep-todo-keywords ; With C-u it runs `prot-search-git-grep-todo-keywords'
"M-s M-T" #'prot-search-git-grep-todo-keywords
"M-s s" #'prot-search-outline
"M-s O" #'prot-search-collect
"M-s M-o" #'prot-search-occur-outline
"M-s M-u" #'prot-search-occur-browse-url)
(prot-emacs-keybind isearch-mode-map
"C-<home>" #'isearch-beginning-of-buffer
"C-<end>" #'isearch-end-of-buffer
"<up>" #'isearch-repeat-backward
"<down>" #'isearch-repeat-forward
"<backspace>" #'prot-search-isearch-abort-dwim
"<C-return>" #'prot-search-isearch-other-end)
(setq prot-search-outline-regexp-alist
'((emacs-lisp-mode . "^\\((\\|;;;+ \\)")
(org-mode . "^\\(\\*+ +\\|#\\+[Tt][Ii][Tt][Ll][Ee]:\\)")
(outline-mode . "^\\*+ +")
(emacs-news-view-mode . "^\\*+ +")
(conf-toml-mode . "^\\[")
(markdown-mode . "^#+ +")))
(setq prot-search-todo-keywords
(concat "TODO\\|FIXME\\|NOTE\\|REVIEW\\|XXX\\|KLUDGE"
"\\|HACK\\|WARN\\|WARNING\\|DEPRECATED\\|BUG"))
(prot-emacs-hook
prot-search-outline-hook
(pulsar-recenter-center pulsar-reveal-entry)
nil
pulsar))
(prot-emacs-keybind global-map
"M-s M-%" #'prot-search-replace-markup ; see `prot-search-markup-replacements'
"M-s M-<" #'prot-search-isearch-beginning-of-buffer
"M-s M->" #'prot-search-isearch-end-of-buffer
"M-s g" #'prot-search-grep
"M-s u" #'prot-search-occur-urls
"M-s t" #'prot-search-occur-todo-keywords
"M-s M-t" #'prot-search-grep-todo-keywords ; With C-u it runs `prot-search-git-grep-todo-keywords'
"M-s M-o" #'prot-search-occur-outline
"M-s M-u" #'prot-search-occur-browse-url)
(prot-emacs-keybind isearch-mode-map
"<up>" #'prot-search-isearch-repeat-backward
"<down>" #'prot-search-isearch-repeat-forward
"<backspace>" #'prot-search-isearch-abort-dwim
"<C-return>" #'prot-search-isearch-other-end))
;;; grep and xref
(prot-emacs-configure
(:delay 5)
(setq reb-re-syntax 'read)
(setq xref-file-name-display 'project-relative)
;; All those have been changed for Emacs 28
(setq xref-show-definitions-function #'xref-show-definitions-completing-read) ; for M-.
(setq xref-show-xrefs-function #'xref-show-definitions-buffer) ; for grep and the like
;; The `outline-xref' is for Emacs 32
(with-eval-after-load 'outline
(define-advice outline-xref (:around (&rest args) prot)
(let ((xref-show-xrefs-function #'xref-show-definitions-completing-read))
(apply args))))
(setq xref-file-name-display 'project-relative)
(setq grep-save-buffers nil)
(setq grep-use-headings nil) ; Emacs 30
(setq grep-use-headings t) ; Emacs 30
(if-let* ((ripgrep (or (executable-find "rg") (executable-find "ripgrep"))))
(progn
(setq grep-program ripgrep)
(setq xref-search-program 'ripgrep)
(setq grep-template (format "%s -nH --null -e <R> <F>" ripgrep)))
(error "You need to install RIPGREP"))
(add-hook 'grep-mode #'prot-common-truncate-lines-silently))
(let ((executable (or (executable-find "rg") "grep"))
(rgp (string-match-p "rg" grep-program)))
(setq grep-program executable)
(setq grep-template
(if rgp
"/usr/bin/rg -nH --null -e <R> <F>"
"/usr/bin/grep <X> <C> -nH --null -e <R> <F>"))
(setq xref-search-program (if rgp 'ripgrep 'grep))))
;;; wgrep (writable grep)
;; See the `grep-edit-mode' for the new built-in feature.
(unless (>= emacs-major-version 31)
(prot-emacs-configure
(prot-emacs-install wgrep)
(with-eval-after-load 'grep
(prot-emacs-keybind grep-mode-map
"e" #'wgrep-change-to-wgrep-mode
"C-x C-q" #'wgrep-change-to-wgrep-mode
"C-c C-c" #'wgrep-finish-edit)
(setq wgrep-auto-save-buffer t)
(setq wgrep-change-readonly-file t))))
(prot-emacs-package wgrep
(:install t)
(:delay 5)
(setq wgrep-auto-save-buffer t)
(setq wgrep-change-readonly-file t)
(prot-emacs-keybind grep-mode-map
"e" #'wgrep-change-to-wgrep-mode
"C-x C-q" #'wgrep-change-to-wgrep-mode
"C-c C-c" #'wgrep-finish-edit))
(provide 'prot-emacs-search)

View file

@ -1,59 +1,100 @@
;;; The Standard themes
(prot-emacs-configure
(prot-emacs-install standard-themes)
;; The themes are customisable. Read the manual:
;; <https://protesilaos.com/emacs/standard-themes>.
(standard-themes-take-over-modus-themes-mode 1)
(prot-emacs-package standard-themes
(:install t)
(:delay 1)
(setq standard-themes-bold-constructs t
standard-themes-italic-constructs t
standard-themes-mixed-fonts t
standard-themes-variable-pitch-ui t
standard-themes-mode-line-accented nil
(prot-emacs-keybind global-map
"<f5>" #'modus-themes-rotate
"C-<f5>" #'modus-themes-select
"M-<f5>" #'modus-themes-load-random)
;; Accepts a symbol value
standard-themes-fringes 'subtle
(setq modus-themes-variable-pitch-ui t
modus-themes-mixed-fonts t
modus-themes-to-rotate nil ; defaults to the return value of `modus-themes-get-themes'
modus-themes-headings ; read the manual's entry of the doc string
;; The following accept lists of properties
standard-themes-links nil
standard-themes-region nil
standard-themes-prompts nil
;; more complex alist to set weight, height, and optional
;; `variable-pitch' per heading level (t is for any level not
;; specified)
standard-themes-headings
'((0 . (variable-pitch light 1.9))
(1 . (variable-pitch light 1.8))
(2 . (variable-pitch regular 1.7))
(3 . (variable-pitch regular 1.6))
(4 . (variable-pitch regular 1.5))
(5 . (variable-pitch 1.4)) ; absence of weight means `bold'
(2 . (variable-pitch light 1.7))
(3 . (variable-pitch semilight 1.6))
(4 . (variable-pitch semilight 1.5))
(5 . (variable-pitch 1.4))
(6 . (variable-pitch 1.3))
(7 . (variable-pitch 1.2))
(agenda-date . (semilight 1.5))
(agenda-structure . (variable-pitch light 1.9))
(agenda-date . (1.3))
(agenda-structure . (variable-pitch light 1.8))
(t . (variable-pitch 1.1))))
;; The `standard-themes' provide lots of themes. I want to pick one at
;; random when I start Emacs: the `modus-themes-load-random' does just
;; that (it can be called interactively as well). I just check with
;; my desktop environment to determine if the choice should be about
;; a light or a dark theme. Those functions are in my init.el.
(if (prot-emacs-gnome-prefers-dark-p)
(modus-themes-load-random 'dark)
(modus-themes-load-random 'light)))
;; Load a theme that is consistent with my session's theme. Those
;; functions are defined in my init.el.
(if (prot-emacs-theme-environment-dark-p)
(standard-themes-load-dark)
(standard-themes-load-light))
;; For testing purposes...
(prot-emacs-comment
(:eval nil)
(define-key global-map (kbd "<f5>") #'standard-themes-toggle))
(add-to-list 'load-path "~/Git/Projects/modus-themes/")
(add-to-list 'load-path "~/Git/Projects/standard-themes/")
(require 'modus-themes)
(require 'standard-themes)
(standard-themes-take-over-modus-themes-mode 1)
(prot-emacs-keybind global-map
"<f5>" #'modus-themes-rotate
"C-<f5>" #'modus-themes-select
"M-<f5>" #'modus-themes-load-random)
(if (prot-emacs-gnome-prefers-dark-p)
(modus-themes-load-random 'dark)
(modus-themes-load-random 'light)))
;; ;; NOTE: For testing purposes
;; (progn
;; (mapc #'disable-theme custom-enabled-themes)
;;
;; (add-to-list 'load-path "/home/prot/Git/Projects/standard-themes/")
;;
;; (require 'standard-themes)
;; (load-theme 'standard-dark t t)
;; (load-theme 'standard-light t t)
;;
;; (setq standard-themes-bold-constructs t
;; standard-themes-italic-constructs t
;; standard-themes-disable-other-themes t
;; standard-themes-mixed-fonts t
;; standard-themes-variable-pitch-ui t
;; standard-themes-mode-line-accented nil
;;
;; ;; Accepts a symbol value
;; standard-themes-fringes 'subtle
;;
;; ;; The following accept lists of properties
;; standard-themes-links nil
;; standard-themes-region nil
;; standard-themes-prompts nil
;;
;; ;; more complex alist to set weight, height, and optional
;; ;; `variable-pitch' per heading level (t is for any level not
;; ;; specified)
;; standard-themes-headings
;; '((0 . (variable-pitch light 1.9))
;; (1 . (variable-pitch light 1.8))
;; (2 . (variable-pitch light 1.7))
;; (3 . (variable-pitch semilight 1.6))
;; (4 . (variable-pitch semilight 1.5))
;; (5 . (variable-pitch 1.4))
;; (6 . (variable-pitch 1.3))
;; (7 . (variable-pitch 1.2))
;; (agenda-date . (1.3))
;; (agenda-structure . (variable-pitch light 1.8))
;; (t . (variable-pitch 1.1))))
;;
;; (mapcar (lambda (theme)
;; (add-to-list
;; 'custom-theme-load-path
;; (concat "/home/prot/Git/Projects/standard-themes/" (symbol-name theme) "-theme.el")))
;; (standard-themes--list-enabled-themes))
;;
;; (if (prot-emacs-theme-environment-dark-p)
;; (load-theme 'standard-dark :no-confirm)
;; (load-theme 'standard-light :no-confirm))
;;
;; (define-key global-map (kbd "<f5>") #'standard-themes-toggle))
(provide 'prot-emacs-standard-themes)

View file

@ -2,333 +2,163 @@
;;;; Load the desired theme module
;; These all reference my packages: `modus-themes', `ef-themes',
;; `doric-themes', `standard-themes'.
;; `standard-themes'.
(when prot-emacs-load-theme-family
(require
(pcase prot-emacs-load-theme-family
('modus 'prot-emacs-modus-themes)
('ef 'prot-emacs-ef-themes)
('doric 'prot-emacs-doric-themes)
('modus 'prot-emacs-modus-themes)
('standard 'prot-emacs-standard-themes))))
;;;; Pulsar
;; Read the pulsar manual: <https://protesilaos.com/emacs/pulsar>.
(prot-emacs-configure
(prot-emacs-install pulsar)
(prot-emacs-package pulsar
(:install t)
(:delay 1)
(dolist (cmd '( narrow-to-page narrow-to-defun
narrow-to-region widen
logos-forward-page-dwim
logos-backward-page-dwim))
(add-to-list 'pulsar-pulse-functions cmd))
(setopt pulsar-pulse t
pulsar-delay 0.055
pulsar-iterations 10
pulsar-face 'pulsar-magenta
pulsar-highlight-face 'pulsar-cyan)
(pulsar-global-mode 1)
(prot-emacs-hook
(next-error-hook minibuffer-setup-hook)
(pulsar-pulse-line-red pulsar-recenter-top pulsar-reveal-entry))
;; There are convenience functions/commands which pulse the line using
;; a specific colour: `pulsar-pulse-line-red' is one of them.
(add-hook 'next-error-hook #'pulsar-pulse-line-red)
(setq pulsar-delay 0.055)
(setq pulsar-iterations 5)
(setq pulsar-face 'pulsar-green)
(setq pulsar-region-face 'pulsar-yellow)
(setq pulsar-highlight-face 'pulsar-magenta)
(add-hook 'minibuffer-setup-hook #'pulsar-pulse-line-red)
;; pulsar does not define any key bindings. This is just my personal
;; preference. Remember to read the manual on the matter. Evaluate:
;;
;; (info "(elisp) Key Binding Conventions")
(prot-emacs-keybind global-map
"C-x l" #'pulsar-pulse-line ; override `count-lines-page'
"C-x L" #'pulsar-highlight-permanently-dwim)) ; or use `pulsar-highlight-temporarily-dwim'
"C-x L" #'pulsar-highlight-dwim)) ; or use `pulsar-highlight-line'
;;;; Lin
;; Read the lin manual: <https://protesilaos.com/emacs/lin>.
(prot-emacs-configure
(prot-emacs-install lin)
(setq lin-face 'lin-slate)
(lin-global-mode 1)
(when (string= (getenv "DESKTOP_SESSION") "gnome")
(lin-gnome-accent-color-mode 1)))
(prot-emacs-package lin
(:install t)
(:delay 1)
;; You can use this to live update the face:
;;
;; (customize-set-variable 'lin-face 'lin-green)
;;
;; Or `setopt' on Emacs 29: (setopt lin-face 'lin-yellow)
;;
;; I still prefer `setq' for consistency.
(setq lin-face 'lin-magenta)
(lin-global-mode 1)) ; applies to all `lin-mode-hooks'
;;;; Increase padding of windows/frames
;; Yet another one of my packages:
;; <https://protesilaos.com/codelog/2023-06-03-emacs-spacious-padding/>.
(when prot-display-graphic-p
(prot-emacs-configure
(prot-emacs-install spacious-padding)
(prot-emacs-package spacious-padding
(:install t)
(:delay 1)
(spacious-padding-mode 1)
;; These are the defaults, but I keep it here for visiibility.
(setq spacious-padding-widths
'( :internal-border-width 15
:header-line-width 4
:mode-line-width 6
:tab-width 4
:right-divider-width 30
:scroll-bar-width 8))
(define-key global-map (kbd "<f8>") #'spacious-padding-mode)
;; Read the doc string of `spacious-padding-subtle-mode-line' as it
;; is very flexible.
(setq spacious-padding-subtle-mode-line
'(:mode-line-active default :mode-line-inactive vertical-border))
(setq spacious-padding-widths
`( :internal-border-width 15
:header-line-width 4
:mode-line-width 6
:tab-width 4
:right-divider-width 15
:scroll-bar-width ,(if prot-pgtk-p 12 6)
:left-fringe-width 8
:right-fringe-width 20))
(spacious-padding-mode 1)
;; (setq spacious-padding-subtle-mode-line nil)
(define-key global-map (kbd "<f8>") #'spacious-padding-mode))
;; Read the doc string of `spacious-padding-subtle-mode-line' as it
;; is very flexible. Here we make the mode lines be a single
;; overline, while header lines have an underline.
(setq spacious-padding-subtle-frame-lines
'( :mode-line-active spacious-padding-line-active
:mode-line-inactive spacious-padding-line-inactive
:header-line-active spacious-padding-line-active
:header-line-inactive spacious-padding-line-inactive))
;;;; Rainbow mode for colour previewing (rainbow-mode.el)
(prot-emacs-package rainbow-mode
(:install t)
(:delay 10)
(setq rainbow-ansi-colors nil)
(setq rainbow-x-colors nil)
(when (< emacs-major-version 29)
(setq x-underline-at-descent-line (when spacious-padding-subtle-frame-lines t)))))
(defun prot/rainbow-mode-in-themes ()
(when-let ((file (buffer-file-name))
((derived-mode-p 'emacs-lisp-mode))
((string-match-p "-theme" file)))
(rainbow-mode 1)))
(add-hook 'emacs-lisp-mode-hook #'prot/rainbow-mode-in-themes)
(define-key ctl-x-x-map "c" #'rainbow-mode)) ; C-x x c
;;; Cursor appearance (cursory)
;; Read the manual: <https://protesilaos.com/emacs/cursory>.
(when prot-display-graphic-p
(prot-emacs-configure
(prot-emacs-install cursory)
(prot-emacs-package cursory
(:install t)
(:delay 1)
(setq cursory-presets
'((box
:blink-cursor-interval 1.2)
(box-no-blink
:blink-cursor-mode -1)
(bar
:cursor-type (bar . 2)
:blink-cursor-interval 0.5)
(bar-no-other-window
:inherit bar
:cursor-in-non-selected-windows nil)
(underscore
:cursor-type (hbar . 3)
:blink-cursor-blinks 50)
(underscore-thin-other-window
:inherit underscore
:cursor-in-non-selected-windows (hbar . 1))
(underscore-thick
:cursor-type (hbar . 8)
:blink-cursor-interval 0.3
:blink-cursor-blinks 50
:cursor-in-non-selected-windows (hbar . 3))
(t ; the default values
:cursor-type box
:cursor-in-non-selected-windows hollow
:blink-cursor-mode 1
:blink-cursor-blinks 10
:blink-cursor-interval 0.2
:blink-cursor-delay 0.2)))
(require 'cursory)
;; I am using the default values of `cursory-latest-state-file'.
(setq cursory-presets
'((box
:blink-cursor-interval 1.2)
(box-no-blink
:inherit box
:blink-cursor-mode -1)
(bar
:cursor-type (bar . 2)
:cursor-color error ; will typically be red
:blink-cursor-interval 0.8)
(bar-no-other-window
:inherit bar
:cursor-in-non-selected-windows nil)
(bar-no-blink
:inherit bar
:blink-cursor-mode -1)
(underscore
:cursor-color warning ; will typically be yellow
:cursor-type (hbar . 3)
:blink-cursor-blinks 50)
(underscore-no-other-window
:inherit underscore
:cursor-in-non-selected-windows nil)
(underscore-thick
:inherit underscore
:cursor-type (hbar . 8)
:cursor-in-non-selected-windows (hbar . 3))
(t ; the default values
:cursor-color unspecified ; use the theme's original
:cursor-type box
:cursor-in-non-selected-windows hollow
:blink-cursor-mode 1
:blink-cursor-blinks 10
:blink-cursor-interval 0.2
:blink-cursor-delay 0.2)))
;; Set last preset or fall back to desired style from `cursory-presets'.
(cursory-set-preset (or (cursory-restore-latest-preset) 'box))
(cursory-set-last-or-fallback)
;; The other side of `cursory-restore-latest-preset'.
(add-hook 'kill-emacs-hook #'cursory-store-latest-preset)
;; Persist configurations between Emacs sessions. Also apply the
;; :cursor-color again when swithcing to another theme.
(cursory-mode 1)
;; We have to use the "point" mnemonic, because C-c c is often the
;; suggested binding for `org-capture' and is the one I use as well.
(define-key global-map (kbd "C-c p") #'cursory-set-preset)
(with-eval-after-load 'prot-streaming
(defvar prot/cursory-streaming--last-preset nil
"The last Cursory preset before enabling `prot/cursory-streaming-mode'.")
(defun prot/cursory-streaming-mode ()
"Set the Cursory preset to `box-no-blink' when streaming."
(cond
(prot-streaming-mode
(setq prot/cursory-streaming-mode cursory-last-selected-preset)
(cursory-set-preset 'box-no-blink))
(prot/cursory-streaming-mode
(cursory-set-preset prot/cursory-streaming-mode)
(setq prot/cursory-streaming-mode nil))
(t
(cursory-set-last-or-fallback))))
(add-hook 'prot-streaming-mode-hook #'prot/cursory-streaming-mode))))
;;; GNOME accent theme switcher
(when (and (null prot-emacs-load-theme-family)
(string= (getenv "DESKTOP_SESSION") "gnome"))
(prot-emacs-configure
(prot-emacs-install-vc
gnome-accent-theme-switcher
"https://github.com/protesilaos/gnome-accent-theme-switcher.git")
(prot-emacs-keybind global-map
"<f5>" #'gnome-accent-theme-switcher-toggle-mode
"C-<f5>" #'gnome-accent-theme-switcher-change-accent
"M-<f5>" #'gnome-accent-theme-switcher-load-random-from-current-set)
(gnome-accent-theme-switcher-mode 1)))
;;; Modus themes exporter
(prot-emacs-configure
;; The use the command `modus-themes-exporter-export'.
(prot-emacs-install-vc
modus-themes-exporter
"https://github.com/protesilaos/modus-themes-exporter.git"))
;; We have to use the "point" mnemonic, because C-c c is often the
;; suggested binding for `org-capture' and is the one I use as well.
(define-key global-map (kbd "C-c p") #'cursory-set-preset))
;;;; Theme buffet
(prot-emacs-configure
(prot-emacs-install theme-buffet)
(prot-emacs-package theme-buffet
(:install t)
(:delay 1)
(setq theme-buffet-menu 'end-user)
(setq theme-buffet-end-user
(setq theme-buffet--end-user
'( :night (modus-vivendi ef-dark ef-winter ef-autumn ef-night ef-duo-dark ef-symbiosis)
:morning (modus-operandi ef-light ef-cyprus ef-spring ef-frost ef-duo-light)
:afternoon (modus-operandi-tinted ef-arbutus ef-day ef-kassio ef-summer ef-elea-light ef-maris-light ef-melissa-light ef-trio-light ef-reverie)
:evening (modus-vivendi-tinted ef-rosa ef-elea-dark ef-maris-dark ef-melissa-dark ef-trio-dark ef-dream)))
:afternoon (modus-operandi-tinted ef-day ef-kassio ef-summer ef-elea-light ef-maris-light ef-melissa-light ef-trio-light)
:evening (modus-vivendi-tinted ef-elea-dark ef-maris-dark ef-melissa-dark ef-trio-dark)))
(theme-buffet-timer-hours 1))
;;;; Fontaine (font configurations)
;; Read the manual: <https://protesilaos.com/emacs/fontaine>
(prot-emacs-configure
(prot-emacs-install fontaine)
(fontaine-mode 1)
(prot-emacs-keybind global-map
"C-c f" #'fontaine-set-preset
"C-c F" #'fontaine-toggle-preset)
(setq-default text-scale-remap-header-line t) ; Emacs 28
;; The font family is my design: <https://github.com/protesilaos/aporetic>.
(setq fontaine-presets
'((small
:default-height 80)
(regular) ; like this it uses all the fallback values and is named `regular'
(medium
:default-family "Aporetic Serif Mono"
:default-height 115
:fixed-pitch-family "Aporetic Serif Mono"
:variable-pitch-family "Aporetic Sans")
(large
:default-height 150)
(presentation
:default-height 220)
(jumbo
:inherit medium
:default-height 280)
(t
;; I keep all properties for didactic purposes, but most can be
;; omitted. See the fontaine manual for the technicalities:
;; <https://protesilaos.com/emacs/fontaine>.
:default-family "Aporetic Sans Mono"
:default-weight regular
:default-slant normal
:default-width normal
:default-height 100
:fixed-pitch-family "Aporetic Sans Mono"
:fixed-pitch-weight nil
:fixed-pitch-slant nil
:fixed-pitch-width nil
:fixed-pitch-height 1.0
:fixed-pitch-serif-family nil
:fixed-pitch-serif-weight nil
:fixed-pitch-serif-slant nil
:fixed-pitch-serif-width nil
:fixed-pitch-serif-height 1.0
:variable-pitch-family "Aporetic Serif"
:variable-pitch-weight nil
:variable-pitch-slant nil
:variable-pitch-width nil
:variable-pitch-height 1.0
:mode-line-active-family nil
:mode-line-active-weight nil
:mode-line-active-slant nil
:mode-line-active-width nil
:mode-line-active-height 1.0
:mode-line-inactive-family nil
:mode-line-inactive-weight nil
:mode-line-inactive-slant nil
:mode-line-inactive-width nil
:mode-line-inactive-height 1.0
:header-line-family nil
:header-line-weight nil
:header-line-slant nil
:header-line-width nil
:header-line-height 1.0
:line-number-family nil
:line-number-weight nil
:line-number-slant nil
:line-number-width nil
:line-number-height 1.0
:tab-bar-family nil
:tab-bar-weight nil
:tab-bar-slant nil
:tab-bar-width nil
:tab-bar-height 1.0
:tab-line-family nil
:tab-line-weight nil
:tab-line-slant nil
:tab-line-width nil
:tab-line-height 1.0
:bold-family nil
:bold-slant nil
:bold-weight bold
:bold-width nil
:bold-height 1.0
:italic-family nil
:italic-weight nil
:italic-slant italic
:italic-width nil
:italic-height 1.0
:line-spacing nil)))
(fontaine-set-preset (or (fontaine-restore-latest-preset) 'regular))
(with-eval-after-load 'pulsar
(add-hook 'fontaine-set-preset-hook #'pulsar-pulse-line)))
;;;; Show Font (preview fonts)
;; Read the manual: <https://protesilaos.com/emacs/show-font>
(when prot-display-graphic-p
(prot-emacs-configure
(prot-emacs-install show-font)
(setq show-font-display-buffer-action-alist '(display-buffer-full-frame))
(prot-emacs-keybind global-map
"C-c S s" #'show-font-select-preview
"C-c S l" #'show-font-tabulated)))
;;;;; `variable-pitch-mode' setup
(prot-emacs-configure
(define-key ctl-x-x-map (kbd "v") #'variable-pitch-mode)
(defun prot/enable-variable-pitch ()
(unless (derived-mode-p 'mhtml-mode 'nxml-mode 'yaml-mode)
(when (bound-and-true-p modus-themes-mixed-fonts)
(variable-pitch-mode 1))))
;; NOTE 2022-11-20: This may not cover every case, though it works
;; fine in my workflow. I am still undecided by EWW.
(prot-emacs-hook
(text-mode-hook notmuch-show-mode-hook elfeed-show-mode-hook)
prot/enable-variable-pitch)
;;;;; Resize keys with global effect
;; Emacs 29 introduces commands that resize the font across all
;; buffers (including the minibuffer), which is what I want, as
;; opposed to doing it only in the current buffer. The keys are the
;; same as the defaults.
(prot-emacs-keybind global-map
"C-x C-=" #'global-text-scale-adjust
"C-x C-+" #'global-text-scale-adjust
"C-x C-0" #'global-text-scale-adjust))
(provide 'prot-emacs-theme)

View file

@ -1,45 +1,34 @@
;;; Vertical completion layout (vertico)
(prot-emacs-configure
(prot-emacs-install vertico)
;;; Vertical completion layout
(prot-emacs-package vertico
(:install t)
(:delay 1)
(setq vertico-scroll-margin 0)
(setq vertico-count 10)
(setq vertico-resize t)
(setq vertico-count 5)
(setq vertico-resize nil)
(setq vertico-cycle t)
(with-eval-after-load 'rfn-eshadow
;; This works with `file-name-shadow-mode' enabled. When you are in
;; a sub-directory and use, say, `find-file' to go to your home '~/'
;; or root '/' directory, Vertico will clear the old path to keep
;; only your current input.
(add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy))
(vertico-mode 1)
(setq vertico-group-format
(concat
(propertize (make-string 20 ? ) 'face 'completions-group-separator)
(propertize " %s " 'face 'completions-group-title)
(propertize " " 'face 'completions-group-separator 'display '(space :align-to right))))
(vertico-mode 1))
;; This works with `file-name-shadow-mode' enabled. When you are in
;; a sub-directory and use, say, `find-file' to go to your home '~/'
;; or root '/' directory, Vertico will clear the old path to keep
;; only your current input.
(add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy))
;;; Custom tweaks for vertico (prot-vertico.el)
(prot-emacs-configure
(require 'prot-vertico)
(setq vertico-reverse-candidates nil)
(setq vertico-multiform-commands
`(("consult-\\(.*\\)?\\(find\\|grep\\|ripgrep\\)" ,@prot-vertico-multiform-maximal)))
(prot-emacs-package prot-vertico
(:delay 5)
(setq vertico-multiform-categories
`(;; Maximal
(embark-keybinding ,@prot-vertico-multiform-maximal)
(multi-category ,@prot-vertico-multiform-maximal)
(consult-location ,@prot-vertico-multiform-maximal)
(jinx ,@prot-vertico-multiform-maximal)
(imenu ,@prot-vertico-multiform-maximal)
(theme ,@prot-vertico-multiform-maximal)
(unicode-name ,@prot-vertico-multiform-maximal)
;; Minimal
(file ,@prot-vertico-multiform-minimal
(vertico-sort-function . vertico-sort-directories-first))
(vertico-preselect . prompt)
(vertico-sort-function . prot-vertico-sort-directories-first))
(t ,@prot-vertico-multiform-minimal)))
(vertico-multiform-mode 1)
@ -47,16 +36,13 @@
(prot-emacs-keybind vertico-map
"<left>" #'backward-char
"<right>" #'forward-char
"<next>" #'vertico-next-group
"<prior>" #'vertico-previous-group
"TAB" #'prot-vertico-private-complete
"DEL" #'vertico-directory-delete-char
"M-DEL" #'vertico-directory-delete-word
"M-," #'vertico-quick-insert
"M-." #'vertico-quick-exit)
(prot-emacs-keybind vertico-multiform-map
"RET" #'prot-vertico-private-exit
"<return>" #'prot-vertico-private-exit
"C-n" #'prot-vertico-private-next
"<down>" #'prot-vertico-private-next
"C-p" #'prot-vertico-private-previous

View file

@ -1,166 +1,168 @@
;;;; `browse-url'
;;; Simple HTML Renderer (shr), Emacs Web Wowser (eww), and prot-eww.el
(prot-emacs-configure
(:delay 60)
;;;; `browse-url'
(setq browse-url-browser-function 'eww-browse-url)
(setq browse-url-secondary-browser-function 'browse-url-default-browser))
(setq browse-url-secondary-browser-function 'browse-url-default-browser)
;;;; `goto-addr'
(prot-emacs-configure
(defun prot/goto-address-mode ()
"Enable `goto-address-mode' only in `text-mode' and no derivatives."
(when (eq major-mode 'text-mode)
(goto-address-mode 1)))
(add-hook 'text-mode-hook #'prot/goto-address-mode)
(add-hook 'prog-mode-hook #'goto-address-prog-mode)
(setq goto-address-url-face 'link)
(setq goto-address-url-mouse-face 'highlight)
(setq goto-address-mail-face nil)
(setq goto-address-mail-mouse-face 'highlight))
(setq goto-address-mail-mouse-face 'highlight)
;;;; `shr' (Simple HTML Renderer)
(prot-emacs-configure
;;;; `shr'
(setq shr-use-colors nil) ; t is bad for accessibility
(setq shr-use-fonts nil) ; t is superfluous, given `variable-pitch-mode'
(setq shr-use-fonts nil) ; t is not for me
(setq shr-max-image-proportion 0.6)
(setq shr-image-animate nil) ; No GIFs, thank you!
(setq shr-width fill-column)
(setq shr-width fill-column) ; check `prot-eww-readable'
(setq shr-max-width fill-column)
(setq shr-discard-aria-hidden t)
(setq shr-fill-text nil) ; Emacs 31
(setq shr-cookie-policy nil))
(setq shr-cookie-policy nil)
;;;; `url-cookie'
(prot-emacs-configure
(setq url-cookie-untrusted-urls '(".*")))
;;;; `eww' (Emacs Web Wowser)
(prot-emacs-configure
(with-eval-after-load 'eww
(autoload #'prot-simple-buffers-major-mode "prot-simple")
(prot-emacs-package eww
(:delay 60)
(setq eww-restore-desktop t)
(setq eww-desktop-remove-duplicates t)
(setq eww-header-line-format nil)
(setq eww-search-prefix "https://duckduckgo.com/html/?q=")
(setq eww-download-directory (expand-file-name "~/Documents/eww-downloads"))
(setq eww-suggest-uris
'(eww-links-at-point
thing-at-point-url-at-point))
(setq eww-bookmarks-directory (locate-user-emacs-file "eww-bookmarks/"))
(setq eww-history-limit 150)
(setq eww-use-external-browser-for-content-type
"\\`\\(video/\\|audio\\)") ; On GNU/Linux check your mimeapps.list
(setq eww-browse-url-new-window-is-tab nil)
(setq eww-form-checkbox-selected-symbol "[X]")
(setq eww-form-checkbox-symbol "[ ]")
;; NOTE `eww-retrieve-command' is for Emacs28. I tried the following
;; two values. The first would not render properly some plain text
;; pages, such as by messing up the spacing between paragraphs. The
;; second is more reliable but feels slower. So I just use the
;; default (nil), though I find wget to be a bit faster. In that case
;; one could live with the occasional errors by using `eww-download'
;; on the offending page, but I prefer consistency.
;;
;; '("wget" "--quiet" "--output-document=-")
;; '("chromium" "--headless" "--dump-dom")
(setq eww-retrieve-command nil)
(prot-emacs-keybind eww-mode-map
"S" nil ; unmap `eww-list-buffers'
"b" #'prot-simple-buffers-major-mode ; a general version to show buffer of current mode
"m" #'bookmark-set)
(define-key eww-link-keymap (kbd "v") nil) ; stop overriding `eww-view-source'
(define-key eww-mode-map (kbd "L") #'eww-list-bookmarks)
(define-key dired-mode-map (kbd "E") #'eww-open-file) ; to render local HTML files
(define-key eww-buffers-mode-map (kbd "d") #'eww-bookmark-kill) ; it actually deletes
(define-key eww-bookmark-mode-map (kbd "d") #'eww-bookmark-kill)) ; same
(define-key eww-link-keymap (kbd "v") nil) ; stop overriding `eww-view-source'
(prot-emacs-package prot-eww
(:delay 60)
(setq prot-eww-save-history-file
(locate-user-emacs-file "prot-eww-visited-history"))
(setq prot-eww-save-visited-history t)
(setq prot-eww-bookmark-link nil)
(with-eval-after-load 'dired
(define-key dired-mode-map (kbd "E") #'eww-open-file)) ; to render local HTML files
(add-hook 'prot-eww-history-mode-hook #'hl-line-mode)
(setq eww-auto-rename-buffer 'title)
(setq eww-header-line-format nil)
(setq eww-bookmarks-directory (locate-user-emacs-file "eww-bookmarks/"))
(setq eww-history-limit 150)
(setq eww-use-external-browser-for-content-type
"\\`\\(video/\\|audio\\)") ; On GNU/Linux check your mimeapps.list
(setq eww-form-checkbox-selected-symbol "[X]")
(setq eww-form-checkbox-symbol "[ ]")
;; NOTE `eww-retrieve-command' is for Emacs28. I tried the following
;; two values. The first would not render properly some plain text
;; pages, such as by messing up the spacing between paragraphs. The
;; second is more reliable but feels slower. So I just use the
;; default (nil), though I find wget to be a bit faster. In that case
;; one could live with the occasional errors by using `eww-download'
;; on the offending page, but I prefer consistency.
;;
;; '("wget" "--quiet" "--output-document=-")
;; '("chromium" "--headless" "--dump-dom")
(setq eww-retrieve-command nil)
;; NOTE 2025-02-15: Emacs has a robust framework for writing
;; bookmarks, which `eww' supports. Though `eww' also defines its
;; own parallel bookmark data, which I do not want to use. So here
;; I disable all the relevant commands.
(dolist (command '( eww-list-bookmarks eww-add-bookmark eww-bookmark-mode
eww-list-buffers eww-toggle-fonts eww-toggle-colors
eww-switch-to-buffer))
(put command 'disabled t))))
;;;; `prot-eww' extras
(prot-emacs-configure
(with-eval-after-load 'eww
(require 'prot-eww)
(prot-emacs-keybind eww-mode-map
"F" #'prot-eww-find-feed
"o" #'prot-eww-open-in-other-window
"j" #'prot-eww-jump-to-url-on-page
"J" #'prot-eww-visit-url-on-page)))
(define-prefix-command 'prot-eww-map)
(define-key global-map (kbd "C-c w") 'prot-eww-map)
(prot-emacs-keybind prot-eww-map
"b" #'prot-eww-visit-bookmark
"e" #'prot-eww-browse-dwim
"s" #'prot-eww-search-engine)
(prot-emacs-keybind eww-mode-map
"B" #'prot-eww-bookmark-page
"D" #'prot-eww-download-html
"F" #'prot-eww-find-feed
"H" #'prot-eww-list-history
"b" #'prot-eww-visit-bookmark
"e" #'prot-eww-browse-dwim
"o" #'prot-eww-open-in-other-window
"E" #'prot-eww-visit-url-on-page
"J" #'prot-eww-jump-to-url-on-page
"R" #'prot-eww-readable
"Q" #'prot-eww-quit))
;;; Elfeed feed/RSS reader
(prot-emacs-configure
(prot-emacs-install elfeed)
(prot-emacs-package elfeed
(:install t)
(:delay 60)
(setq elfeed-use-curl nil)
(setq elfeed-curl-max-connections 10)
(setq elfeed-db-directory (concat user-emacs-directory "elfeed/"))
(setq elfeed-enclosure-default-dir "~/Downloads/")
(setq elfeed-search-filter "@2-weeks-ago +unread")
(setq elfeed-sort-order 'descending)
(setq elfeed-search-clipboard-type 'CLIPBOARD)
(setq elfeed-search-title-max-width 100)
(setq elfeed-search-title-min-width 30)
(setq elfeed-search-trailing-width 25)
(setq elfeed-show-truncate-long-urls t)
(setq elfeed-show-unique-buffers t)
(setq elfeed-search-date-format '("%F %R" 16 :left))
;; Make sure to also check the section on shr and eww for how I handle
;; `shr-width' there.
(add-hook 'elfeed-show-mode-hook
(lambda () (setq-local shr-width (current-fill-column))))
(define-key global-map (kbd "C-c e") #'elfeed)
(prot-emacs-keybind elfeed-search-mode-map
"w" #'elfeed-search-yank
"g" #'elfeed-update
"G" #'elfeed-search-update--force)
(with-eval-after-load 'elfeed
(define-key elfeed-show-mode-map (kbd "w") #'elfeed-show-yank))
(add-hook 'elfeed-show-mode-hook #'visual-line-mode)
(with-eval-after-load 'elfeed
(prot-emacs-package prot-elfeed
(setq prot-elfeed-tag-faces t)
(prot-elfeed-fontify-tags)
(add-hook 'elfeed-search-mode-hook #'prot-elfeed-load-feeds)
(prot-emacs-keybind elfeed-search-mode-map
"w" #'elfeed-search-yank
"g" #'elfeed-update
"G" #'revert-buffer)
"s" #'prot-elfeed-search-tag-filter
"+" #'prot-elfeed-toggle-tag)
(define-key elfeed-show-mode-map (kbd "w") #'elfeed-show-yank)
(setq elfeed-last-update-relative 0) ; absolute time in the header-line
(setq elfeed-search-separator-date-format nil) ; no date sections in the search buffer
(setq elfeed-use-curl nil)
(setq elfeed-curl-max-connections 10)
(setq elfeed-db-directory (expand-file-name "elfeed/" user-emacs-directory))
(setq elfeed-enclosure-default-dir (expand-file-name "~/Downloads/"))
(setq-default elfeed-search-filter "@2-weeks-ago +unread") ; NOTE 2026-05-28: was changed to buffer-local
(setq elfeed-sort-order 'descending)
(setq elfeed-search-clipboard-type 'CLIPBOARD)
(setq elfeed-search-title-max-width 100)
(setq elfeed-search-title-min-width 30)
(setq elfeed-search-trailing-width 25)
(setq elfeed-show-truncate-long-urls t)
(setq elfeed-show-unique-buffers t)
(setq elfeed-search-date-format '("%F %R" 20 :left))
;; NOTE 2025-10-28: Instead of showing my `elfeed-feeds' here, I put
;; it in its own Emacs Lisp file. The `load' function will evaluate
;; that code. Internally, the file has just the following:
;;
;; (setq elfeed-feeds FEEDS)
(unless (bound-and-true-p elfeed-feeds)
(load (expand-file-name "~/feeds.el.gpg") :no-error :no-message))
;; Make entries tagged as "personal" use the `bold-italic' face.
;; This way I can quickly notice my own entries in the listing.
(add-to-list 'elfeed-search-face-alist '(personal bold-italic))
;; NOTE 2026-05-29: Make `elfeed-search-yank' mark the entry as
;; read. If I am copying the URL, then it means I already plan to
;; do something with this entry outside of Elfeed.
(define-advice elfeed-search-yank (:around (&rest args) prot)
(save-excursion
(apply args))
(call-interactively 'elfeed-search-untag-all-unread))))
(define-key elfeed-show-mode-map (kbd "+") #'prot-elfeed-toggle-tag)))
;;; Rcirc (IRC client)
(prot-emacs-configure
(with-eval-after-load 'rcirc
(setq rcirc-server-alist
`(("irc.libera.chat"
:channels ("#emacs")
:port 6697
:encryption tls
:password ,(prot-common-auth-get-field "libera" :secret))))
(setq rcirc-prompt "%t> ") ; Read the docs or use (customize-set-variable 'rcirc-prompt "%t> ")
;; NOTE 2023-04-24: I seldom use IRC. When I have to, I simply
;; evaluate the following. Otherwise I do not need to load it each
;; time.
(setq rcirc-default-nick "protesilaos"
rcirc-default-user-name "protesilaos"
rcirc-default-full-name "Protesilaos")
;; ;; NOTE 2021-11-28: demo from the days of EmacsConf 2021. I don't
;; ;; actually need this.
;; (setq rcirc-bright-nicks '("bandali" "sachac" "zaeph"))
;; NOTE 2021-11-28: Is there a canonical way to disable this?
(setq rcirc-timeout-seconds most-positive-fixnum)
(rcirc-track-minor-mode 1)))
;; (prot-emacs-package rcirc
;; (:delay 10)
;;
;; (setq rcirc-server-alist
;; `(("irc.libera.chat"
;; :channels ("#emacs" "#tropin" "#rcirc")
;; :port 6697
;; :encryption tls
;; :password ,(prot-common-auth-get-field "libera" :secret))))
;;
;; (setq rcirc-prompt "%t> ") ; Read the docs or use (customize-set-variable 'rcirc-prompt "%t> ")
;;
;; (setq rcirc-default-nick "protesilaos"
;; rcirc-default-user-name rcirc-default-nick
;; rcirc-default-full-name "Protesilaos Stavrou")
;;
;; ;; ;; NOTE 2021-11-28: demo from the days of EmacsConf 2021. I don't
;; ;; ;; actually need this.
;; ;; (setq rcirc-bright-nicks '("bandali" "sachac" "zaeph"))
;;
;; ;; NOTE 2021-11-28: Is there a canonical way to disable this?
;; (setq rcirc-timeout-seconds most-positive-fixnum)
;;
;; (rcirc-track-minor-mode 1)
;;
;; (define-key global-map (kbd "C-c i") #'irc))
(provide 'prot-emacs-web)

View file

@ -1,12 +1,14 @@
(prot-emacs-configure
(prot-emacs-package which-key
(:install t)
(:delay 1)
(setq which-key-separator " ")
(setq which-key-prefix-prefix "... ")
(setq which-key-max-display-columns 3)
(setq which-key-idle-delay 1.5)
(setq which-key-idle-delay 0.5)
(setq which-key-idle-secondary-delay 0.25)
(setq which-key-add-column-padding 1)
(setq which-key-max-description-length 40)
(which-key-mode 1)) ; built into Emacs 30
(which-key-mode 1))
(provide 'prot-emacs-which-key)

View file

@ -1,32 +1,25 @@
(prot-emacs-configure
(setq tab-bar-new-button-show nil)
(setq tab-bar-close-button-show nil)
(setq tab-bar-show 1)
(advice-add #'window--delete :after #'prot-common-clear-minibuffer-message))
;;; General window and buffer configurations
(prot-emacs-configure
(:delay 1)
;;;; `uniquify' (unique names for buffers)
(setq uniquify-buffer-name-style 'forward)
(setq uniquify-strip-common-suffix t)
(setq uniquify-after-kill-buffer-p t))
(setq uniquify-after-kill-buffer-p t)
;;;; Line highlight
(prot-emacs-configure
(require 'hl-line)
(setq hl-line-sticky-flag nil)
(setq hl-line-overlay-priority -50)) ; emacs28
(setq hl-line-overlay-priority -50) ; emacs28
;;;; Negative space highlight
(prot-emacs-configure
(require 'whitespace)
;; NOTE 2023-08-14: This is experimental. I am not sure I like it.
(setq whitespace-style
'(face
tabs
spaces
newline
tab-mark
space-mark
newline-mark
trailing
missing-newline-at-eof
space-after-tab::tab
@ -34,30 +27,28 @@
space-before-tab::tab
space-before-tab::space))
(prot-emacs-keybind global-map
"<f6>" #'whitespace-mode
"C-c z" #'delete-trailing-whitespace))
;;; Line numbers on the side of the window
(prot-emacs-configure
(setq display-line-numbers-type t)
;;; Line numbers on the side of the window (prot-line-numbers.el)
(require 'display-line-numbers)
;; Set absolute line numbers, unless evil mode is loaded, in which
;; case we prefer relative numbers.
(setq-default display-line-numbers-type (if prot-emacs-load-evil 'relative t))
;; Those two variables were introduced in Emacs 27.1
(setq display-line-numbers-major-tick 0)
(setq display-line-numbers-minor-tick 0)
;; Use absolute numbers in narrowed buffers
(setq-default display-line-numbers-widen t))
(setq-default display-line-numbers-widen t)
(prot-emacs-configure
(setq prot-streaming-mode-modes '(text-mode prog-mode dired-mode comint-mode))
(prot-emacs-autoload prot-streaming-mode "prot-streaming")
(define-key global-map (kbd "<f7>") #'prot-streaming-mode))
(prot-emacs-keybind global-map
"<f6>" #'whitespace-mode
"<f7>" #'display-line-numbers-mode
"C-c z" #'delete-trailing-whitespace)
;;;; `window', `display-buffer-alist', and related
(prot-emacs-configure
(require 'prot-window)
;; NOTE 2023-03-17: Remember that I am using development versions of
;; Emacs. Some of my `display-buffer-alist' contents are for Emacs
;; 29+.
;; NOTE 2023-03-17: Remember that I am using development versions of
;; Emacs. Some of my `display-buffer-alist' contents are for Emacs
;; 29+.
(setq display-buffer-alist
`(;; no window
("\\`\\*Async Shell Command\\*\\'"
@ -66,7 +57,7 @@
(display-buffer-no-window)
(allow-no-window . t))
;; bottom side window
("\\*\\(Org \\(Select\\|Note\\)\\|Agenda Commands\\)\\*" ; the `org-capture' key selection and `org-add-log-note'
("\\*Org \\(Select\\|Note\\)\\*" ; the `org-capture' key selection and `org-add-log-note'
(display-buffer-in-side-window)
(dedicated . t)
(side . bottom)
@ -78,59 +69,41 @@
(derived-mode . messages-buffer-mode)
(derived-mode . backtrace-mode)))
(display-buffer-reuse-mode-window display-buffer-at-bottom)
(mode . ( flymake-diagnostics-buffer-mode flymake-project-diagnostics-mode
messages-buffer-mode backtrace-mode))
(inhibit-switch-frame . t)
(window-height . 0.3)
(dedicated . t)
(preserve-size . (t . t))
(body-function . select-window))
(preserve-size . (t . t)))
("\\*Embark Actions\\*"
;; NOTE 2026-07-14: I am including `display-buffer-at-bottom' here because Embark can be
;; called from the minibuffer where `display-buffer-below-selected' does not work as intended.
(display-buffer-reuse-mode-window display-buffer-below-selected display-buffer-at-bottom)
(display-buffer-reuse-mode-window display-buffer-below-selected)
(window-height . fit-window-to-buffer)
(window-parameters . ((no-other-window . t)
(mode-line-format . none))))
("\\*\\(Output\\|Register Preview\\).*"
(display-buffer-reuse-mode-window display-buffer-at-bottom)
(inhibit-switch-frame . t))
(display-buffer-reuse-mode-window display-buffer-at-bottom))
;; below current window
("\\*prot-elisp-macroexpand\\*"
(display-buffer-below-selected)
(window-height . 0.3)
("\\(\\*Capture\\*\\|CAPTURE-.*\\)"
(display-buffer-reuse-mode-window display-buffer-below-selected))
("\\*\\vc-\\(incoming\\|outgoing\\|git : \\).*"
(display-buffer-reuse-mode-window display-buffer-below-selected)
(window-height . 0.1)
(dedicated . t)
(preserve-size . (t . t))
(body-function . select-window))
(preserve-size . (t . t)))
((derived-mode . reb-mode) ; M-x re-builder
(display-buffer-reuse-mode-window display-buffer-below-selected)
(inhibit-switch-frame . t)
(window-height . 4) ; note this is literal lines, not relative
(dedicated . t)
(preserve-size . (t . t)))
((or . ((derived-mode . occur-mode)
(derived-mode . grep-mode)
(derived-mode . Buffer-menu-mode)
(derived-mode . log-view-mode)
(derived-mode . help-mode) ; See the hooks for `visual-line-mode'
"\\*\\(|Buffer List\\|Occur\\|vc-change-log\\|eldoc.*\\).*"))
"\\*\\(|Buffer List\\|Occur\\|vc-change-log\\).*"
prot-window-shell-or-term-p
,world-clock-buffer-name))
(prot-window-display-buffer-below-or-pop)
(body-function . prot-window-select-fit-size))
(prot-window-shell-or-term-p
(display-buffer-reuse-mode-window display-buffer-at-bottom)
(mode . (shell-mode eshell-mode comint-mode))
(inhibit-switch-frame . t)
(body-function . prot-window-select-fit-size))
((derived-mode . vc-compilation-mode)
(display-buffer-reuse-mode-window display-buffer-below-selected)
(inhibit-switch-frame . t)
(dedicated . t)
(window-height . 0.1))
((or . ((derived-mode . calendar-mode)
(derived-mode . ert-results-mode)))
(body-function . prot-window-select-fit-size))
("\\*\\(Calendar\\|Bookmark Annotation\\|ert\\).*"
(display-buffer-reuse-mode-window display-buffer-below-selected)
(mode . (calendar-mode ert-results-mode))
(inhibit-switch-frame . t)
(dedicated . t)
(window-height . fit-window-to-buffer))
;; NOTE 2022-09-10: The following is for `ispell-word', though
@ -138,51 +111,36 @@
;; with `prot-spell-ispell-display-buffer' and change the
;; value of `ispell-choices-buffer'.
("\\*ispell-top-choices\\*.*"
(display-buffer-below-selected)
(display-buffer-reuse-mode-window display-buffer-below-selected)
(window-height . fit-window-to-buffer))
;; current window
((or . (;; NOTE 2023-02-17: `man' does not fully obey the
;; `display-buffer-alist'. It works for new frames
;; and for `display-buffer-below-selected', but
;; otherwise is unpredictable. See `Man-notify-method'.
(derived-mode . Man-mode)
;; same window
;; NOTE 2023-02-17: `man' does not fully obey the
;; `display-buffer-alist'. It works for new frames and for
;; `display-buffer-below-selected', but otherwise is
;; unpredictable. See `Man-notify-method'.
((or . ((derived-mode . Man-mode)
(derived-mode . woman-mode)
"\\*\\(Man\\|woman\\).*"))
(display-buffer-reuse-mode-window display-buffer-same-window))
((or . ((derived-mode . magit-diff-mode)
(derived-mode . magit-log-mode)))
(display-buffer-reuse-mode-window prot-window-display-new display-buffer-pop-up-window)
(inhibit-switch-frame . t))
;; other `tab-bar-mode' tab
((or . ((derived-mode . magit-status-mode)
(derived-mode . vc-dir-mode)
"\\*.*vc-dir.*\\*"))
(display-buffer-reuse-mode-window display-buffer-in-tab)
(mode . (magit-status-mode vc-dir-mode))
(tab-name . (lambda (buffer _alist) (buffer-name buffer)))
(reusable-frames . :just-the-selected-frame)
(inhibit-switch-frame . t)))))
(display-buffer-same-window))))
(prot-emacs-configure
(setq split-window-preferred-direction 'horizontal) ; Emacs 31
(setq window-combination-resize t)
(setq even-window-sizes 'height-only)
(setq window-sides-vertical nil)
(setq switch-to-buffer-in-dedicated-window 'pop)
(setq split-height-threshold 85)
(setq split-height-threshold 80)
(setq split-width-threshold 125)
(setq window-min-height 3)
(setq window-min-width 30))
(setq window-min-width 30)
(prot-emacs-configure
(prot-emacs-hook (epa-info-mode-hook help-mode-hook custom-mode-hook) visual-line-mode))
(dolist (hook '(epa-info-mode-hook help-mode-hook custom-mode-hook))
(add-hook hook #'visual-line-mode))
(prot-emacs-configure
(prot-emacs-hook (world-clock-mode-hook calendar-mode-hook) prot-common-truncate-lines-silently))
(add-hook 'world-clock-mode-hook #'prot-common-truncate-lines-silently)
(add-hook 'calendar-mode-hook #'prot-common-truncate-lines-silently)
(prot-emacs-configure
;; NOTE 2022-09-17: Also see `prot-simple-swap-window-buffers'.
(prot-emacs-keybind global-map
;; NOTE 2022-09-17: Also see `prot-simple-swap-window-buffers'.
"C-x <down>" #'next-buffer
"C-x <up>" #'previous-buffer
"C-x C-n" #'next-buffer ; override `set-goal-column'
@ -202,68 +160,50 @@
;;; Frame-isolated buffers
;; Another package of mine. Read the manual:
;; <https://protesilaos.com/emacs/beframe>.
(prot-emacs-configure
(prot-emacs-install beframe)
(require 'beframe)
(prot-emacs-package beframe
(:install t)
(:delay 1)
(setq beframe-functions-in-frames '(project-prompt-project-dir))
(beframe-mode 1)
(prot-emacs-keybind global-map
;; Override the `set-fill-column' that I have no use for.
"C-x f" #'other-frame-prefix
;; Bind Beframe commands to a prefix key. Notice the -map as I am
;; binding keymap here, not a command.
"C-c b" #'beframe-prefix-map
"C-c b" beframe-prefix-map
;; Replace the generic `buffer-menu'. With a prefix argument, this
;; commands prompts for a frame. Call the `buffer-menu' via M-x if
;; you absolutely need the global list of buffers.
"C-x C-b" #'beframe-buffer-menu
;; Not specific to Beframe, but since it renames frames (by means
;; of `beframe-mode') it is appropriate to have this here:
"C-x B" #'select-frame-by-name)
"C-x B" #'select-frame-by-name))
(beframe-mode 1)
;;; Window history (winner-mode)
(prot-emacs-package winner
(:delay 5)
(winner-mode 1)
(with-eval-after-load 'consult
(defface beframe-buffer
'((t :inherit font-lock-string-face))
"Face for `consult' framed buffers.")
(defun prot/beframe-buffer-names-sorted (&optional frame)
"Return the list of buffers from `beframe-buffer-names' sorted by visibility.
With optional argument FRAME, return the list of buffers of FRAME."
(beframe-buffer-names frame :sort #'beframe-buffer-sort-visibility))
(defvar beframe-consult-source
`( :name "Frame-specific buffers (current frame)"
:narrow ?F
:category buffer
:face beframe-buffer
:history beframe-history
:items ,#'prot/beframe-buffer-names-sorted
:action ,#'switch-to-buffer
:state ,#'consult--buffer-state))
(add-to-list 'consult-buffer-sources 'beframe-consult-source)))
(prot-emacs-keybind global-map
"C-x <right>" #'winner-redo
"C-x <left>" #'winner-undo))
;;; Frame history (undelete-frame-mode)
(prot-emacs-configure
(:delay 5)
(define-key global-map (kbd "C-x u") #'undelete-frame) ; I use only C-/ for `undo'
(undelete-frame-mode 1))
;;; Window history (winner-mode)
(prot-emacs-configure
(prot-emacs-keybind global-map
"C-x <right>" #'winner-redo
"C-x <left>" #'winner-undo)
(winner-mode 1))
;;; Directional window motions (windmove)
(prot-emacs-configure
;; Those override some commands that are already available with
;; C-M-u, C-M-f, C-M-b.
(prot-emacs-package windmove
(:delay 5)
(setq windmove-create-window nil) ; Emacs 27.1
(prot-emacs-keybind global-map
;; Those override some commands that are already available with
;; C-M-u, C-M-f, C-M-b.
"C-M-<up>" #'windmove-up
"C-M-<right>" #'windmove-right
"C-M-<down>" #'windmove-down
@ -271,51 +211,17 @@ With optional argument FRAME, return the list of buffers of FRAME."
"C-M-S-<up>" #'windmove-swap-states-up
"C-M-S-<right>" #'windmove-swap-states-right ; conflicts with `org-increase-number-at-point'
"C-M-S-<down>" #'windmove-swap-states-down
"C-M-S-<left>" #'windmove-swap-states-left)
"C-M-S-<left>" #'windmove-swap-states-left))
(setq windmove-create-window nil)) ; Emacs 27.1
;;; Header line context of symbol/heading (breadcrumb.el)
(prot-emacs-package breadcrumb
(:install t)
(:delay 2)
(setq bc-project-max-length 0.5)
(setq bc-project-crumb-separator "/")
(setq bc-imenu-max-length 1.0)
(setq bc-imenu-crumb-separator " > ")
;;; Buffer to PDF
(prot-emacs-configure
;; The use the command `buffer-to-pdf' or `buffer-to-pdf-black-on-white'.
(prot-emacs-install-vc
buffer-to-pdf
"https://github.com/protesilaos/buffer-to-pdf.git"))
(prot-emacs-configure
(setq speedbar-use-images nil) ; t here refers to the `cdr' of each `speedbar-expand-image-button-alist'
(setq speedbar-smart-directory-expand-flag nil)
(setq speedbar-prefer-window t ; both of these are for Emacs 31
speedbar-window-max-width 60)
(setq speedbar-indentation-width 4)
(setq speedbar-show-unknown-files t)
;; NOTE 2026-08-05 17:08 +0300: Waiting for https://debbugs.gnu.org/cgi/bugreport.cgi?bug=81556 which I just submitted.
(setq speedbar-directory-unshown-regexp nil
speedbar-file-unshown-regexp nil)
(prot-emacs-hook speedbar-visiting-tag-hook (speedbar-recenter speedbar-highlight-one-tag-line))
(defun prot/speedbar-imenu-flatten (elements)
"Flatten the Imenu ELEMENTS."
(let* ((elements (imenu--flatten-index-alist elements t))
(elements-no-prefix (mapcar
(lambda (element)
(when-let* ((string (car element))
(position (cdr element))
(_ (or (natnump position) (markerp position))))
(cond
((string-match-p ":.*:" string)
(cons
(string-join (cdr (split-string string "[:]")))
position))
((string-match-p ":" string)
(cons
(cadr (split-string string "[:]"))
position))
(t
element))))
elements)))
(delq nil elements-no-prefix)))
(setq speedbar-tag-hierarchy-method '(prot/speedbar-imenu-flatten)))
(breadcrumb-mode 1))
(provide 'prot-emacs-window)

File diff suppressed because it is too large Load diff

View file

@ -1,108 +0,0 @@
;;; prot-abbrev.el --- Functions for use with abbrev-mode -*- lexical-binding: t -*-
;; Copyright (C) 2025-2026 Protesilaos
;; Author: Protesilaos <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or (at
;; your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Functions for use with `abbrev-mode'.
;;; Code:
(defgroup prot-abbrev ()
"Functions for use with `abbrev-mode'."
:group 'editing)
(defcustom prot-abbrev-time-specifier "%R"
"Time specifier for `format-time-string'."
:type 'string
:group 'prot-abbrev)
(defcustom prot-abbrev-date-specifier "%F"
"Date specifier for `format-time-string'."
:type 'string
:group 'prot-abbrev)
(defun prot-abbrev-current-time ()
"Insert the current time per `prot-abbrev-time-specifier'."
(insert (format-time-string prot-abbrev-time-specifier)))
(defun prot-abbrev-current-date ()
"Insert the current date per `prot-abbrev-date-specifier'."
(insert (format-time-string prot-abbrev-date-specifier)))
(defun prot-abbrev-jitsi-link ()
"Insert a Jitsi link."
(insert (concat "https://meet.jit.si/" (format-time-string "%Y%m%dT%H%M%S"))))
(defvar prot-abbrev-update-html-history nil
"Minibuffer history for `prot-abbrev-update-html-prompt'.")
(defun prot-abbrev-update-html-prompt ()
"Minibuffer prompt for `prot-abbrev-update-html'.
Use completion among previous entries, retrieving their data from
`prot-abbrev-update-html-history'."
(completing-read
"Insert update for manual: "
prot-abbrev-update-html-history
nil nil nil 'prot-abbrev-update-html-history))
(defun prot-abbrev-update-html ()
"Insert message to update NAME.html page, by prompting for NAME."
(insert (format "Update %s.html" (prot-abbrev-update-html-prompt))))
(defvar prot-abbrev-org-macro-key-history nil
"Minibuffer history for `prot-abbrev-org-macro-key-prompt'.")
(defun prot-abbrev-org-macro-key-prompt ()
"Minibuffer prompt for `prot-abbrev-org-macro-key'.
Use completion among previous entries, retrieving their data from
`prot-abbrev-org-macro-key-history'."
(completing-read
"Key binding: "
prot-abbrev-org-macro-key-history
nil nil nil 'prot-abbrev-org-macro-key-history))
(defvar prot-abbrev-org-macro-key-symbol-history nil
"Minibuffer history for `prot-abbrev-org-macro-key-symbol-prompt'.")
(defun prot-abbrev-org-macro-key-symbol-prompt ()
"Minibuffer prompt for `prot-abbrev-org-macro-key'.
Use completion among previous entries, retrieving their data from
`prot-abbrev-org-macro-key-symbol-history'."
(completing-read
"Command name: "
prot-abbrev-org-macro-key-symbol-history
nil nil nil 'prot-abbrev-org-macro-key-symbol-history))
(defun prot-abbrev-org-macro-key-command ()
"Insert {{{kbd(KEY)}}} (~SYMBOL~) by prompting for KEY and SYMBOL."
(insert (format "{{{kbd(%s)}}} (~%s~)"
(prot-abbrev-org-macro-key-prompt)
(prot-abbrev-org-macro-key-symbol-prompt))))
(defun prot-abbrev-org-macro-key ()
"Insert {{{kbd(KEY)}}} by prompting for KEY."
(insert (format "{{{kbd(%s)}}}" (prot-abbrev-org-macro-key-prompt))))
(provide 'prot-abbrev)
;;; prot-abbrev.el ends here

View file

@ -1,8 +1,8 @@
;;; prot-coach.el --- Code for my personal coaching sessions -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Protesilaos
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -43,11 +43,11 @@
(defun prot-coach--name-prompt-default ()
"Return default value for `prot-coach-name-prompt'."
(when-let* ((from (cond
((derived-mode-p 'message-mode)
(message-fetch-field "To"))
((derived-mode-p 'notmuch-show-mode)
(notmuch-show-get-header :From)))))
(when-let ((from (cond
((derived-mode-p 'message-mode)
(message-fetch-field "To"))
((derived-mode-p 'notmuch-show-mode)
(notmuch-show-get-header :From)))))
(string-clean-whitespace (car (split-string from "<")))))
(defun prot-coach-name-prompt ()
@ -135,7 +135,7 @@ Omit entries with a CANCEL state."
(erase-buffer)
(goto-char (point-min))
(insert "# Type C-c C-c in the TBLFM line to produce the table\n\n")
(insert "* Cost without counting minutes\n")
(insert "* Cost counting without counting minutes\n")
(insert (concat
"\n"
"| Description | Started | Closed | Days | Hours | Cost (EUR) |" "\n"
@ -172,7 +172,7 @@ Omit entries with a CANCEL state."
(let (sessions)
(org-map-entries
(lambda ()
(when-let* ((entry (prot-coach--get-deadline-and-close name)))
(when-let ((entry (prot-coach--get-deadline-and-close name)))
(push entry sessions))))
(prot-coach--print-table-with-sessions name sessions))))

View file

@ -1,8 +1,8 @@
;;; prot-comment.el --- Extensions newcomment.el for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2021-2026 Protesilaos
;; Copyright (C) 2021-2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -26,6 +26,11 @@
;;
;; This covers my newcomment.el extras, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:

View file

@ -1,8 +1,8 @@
;;; prot-common.el --- Common functions for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2020-2026 Protesilaos
;; Copyright (C) 2020-2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -25,11 +25,14 @@
;;; Commentary:
;;
;; Common functions for my Emacs: <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
;; TODO 2025-12-21: I need to review all this.
(eval-when-compile
(require 'subr-x)
(require 'cl-lib))
@ -38,24 +41,6 @@
"Auxiliary functions for my dotemacs."
:group 'editing)
(defvar prot-common-file-extensions
'((archive "tar" "rar" "zip" "xz")
(image "jpg" "png" "jpeg" "webp")
(audio "mp3" "flac" "ogg" "wav")
(video "mkv" "webm" "mp4")
(text "org" "txt" "md")
(document "pdf" "epub" "info" "texi")
(diff "diff" "patch")
(program "sh" "el" "c" "py" "js" "html" "xml")
(program-data "yaml" "yml" "toml" "conf" "json" "css"))
"Some common file extensions grouped by type.")
(defun prot-common--get-file-type-regexp (type)
"Return regular expression for file TYPE.
TYPE is a `car' among the elements of `prot-common-file-extensions'."
(when-let* ((extensions (alist-get type prot-common-file-extensions)))
(format "\\.%s\\'" (regexp-opt extensions))))
;;;###autoload
(defun prot-common-number-even-p (n)
"Test if N is an even number."
@ -142,45 +127,6 @@ floating points: 16.666666666666664 => 16.667."
(prot-common-seconds-to-hours seconds)
(prot-common-seconds-to-minutes seconds)))
(defvar prot-common-temperature-history nil
"Minibuffer history for temperature conversions.")
;;;###autoload
(defun prot-common-fahrenheit-to-celsius (degree)
"Convert Fahrenheit DEGREE to Celsius."
(interactive
(list
(read-number "Fahrenheit DEGREE: " nil 'prot-common-temperature-history)))
(let* ((converted-degrees (/ (- degree 32) 1.8))
(f-string (number-to-string degree))
(c-string (number-to-string converted-degrees)))
(message "%s degrees Fahrenheit is %s degrees Celsius"
(propertize f-string 'face 'error)
(propertize c-string 'face 'success))))
;;;###autoload
(defun prot-common-celsius-to-fahrenheit (degree)
"Convert Celsius DEGREE to Fahrenheit."
(interactive
(list
(read-number "Celsius DEGREE: " nil 'prot-common-temperature-history)))
(let* ((converted-degrees (+ (* degree 1.8) 32))
(c-string (number-to-string degree))
(f-string (number-to-string converted-degrees)))
(message "%s degrees Celsius is %s degrees Fahrenheit"
(propertize c-string 'face 'success)
(propertize f-string 'face 'error))))
;;;###autoload
(defun prot-common--rotate (elements)
"Rotate list ELEMENTS so that nth 0 is last, nth 1 is nth 0, and so on.
Do it without side effects, unlike `prot-common-rotate-list-of-symbol'."
(declare (side-effect-free t))
(setq elements (copy-sequence elements))
(let ((first (car elements))
(rest (cdr elements)))
(append rest (list first))))
;;;###autoload
(defun prot-common-rotate-list-of-symbol (symbol)
"Rotate list value of SYMBOL by moving its car to the end.
@ -221,10 +167,7 @@ will continue rotating accordingly."
;;;###autoload
(defun prot-common-truncate-lines-silently ()
"Toggle line truncation without printing messages."
;; NOTE 2026-08-28: I used to only do `inhibit-message' via a `let',
;; but it was still printing messages.
(cl-letf (((symbol-function 'message) #'always)
(inhibit-message t))
(let ((inhibit-message t))
(toggle-truncate-lines t)))
;; NOTE 2023-08-12: I tried the `clear-message-function', but it did
@ -262,12 +205,6 @@ respectively."
(and (numberp split-height-threshold)
(> (window-total-height) split-height-threshold))))
(defun prot-common-window-narrow-p ()
"Return non-nil if window is narrow.
Check if the `window-width' is less than `split-width-threshold'."
(and (numberp split-width-threshold)
(< (window-total-width) split-width-threshold)))
;;;###autoload
(defun prot-common-three-or-more-windows-p (&optional frame)
"Return non-nil if three or more windows occupy FRAME.
@ -284,7 +221,7 @@ If FRAME is non-nil, inspect the current frame."
;;;###autoload
(defun prot-common-completion-category ()
"Return completion category."
(when-let* ((window (active-minibuffer-window)))
(when-let ((window (active-minibuffer-window)))
(with-current-buffer (window-buffer window)
(completion-metadata-get
(completion-metadata (buffer-substring-no-properties
@ -307,16 +244,6 @@ tools (e.g. `embark')."
`(metadata (category . ,category))
(complete-with-action action candidates string pred))))
;;;###autoload
(defun prot-common-completion-table-no-sort (category candidates)
"Pass appropriate metadata CATEGORY to completion CANDIDATES.
Like `prot-common-completion-table' but also disable sorting."
(lambda (string pred action)
(if (eq action 'metadata)
`(metadata (category . ,category)
(display-sort-function . ,#'identity))
(complete-with-action action candidates string pred))))
;; Thanks to Igor Lima for the `prot-common-crm-exclude-selected-p':
;; <https://github.com/0x462e41>.
;; This is used as a filter predicate in the relevant prompts.
@ -396,7 +323,7 @@ Return the exit code and output in a list."
;;;###autoload
(defun prot-common-auth-get-field (host prop)
"Find PROP in `auth-sources' for HOST entry."
(when-let* ((source (auth-source-search :host host)))
(when-let ((source (auth-source-search :host host)))
(if (eq prop :secret)
(funcall (plist-get (car source) prop))
(plist-get (flatten-list source) prop))))
@ -415,10 +342,6 @@ completion (per `completing-read')."
(buffer-substring-no-properties (point-min) (point-max)))
"\n" :omit-nulls "[\s\f\t\n\r\v]+"))
(defun prot-common-ignore (&rest _)
"Use this as override advice to make a function do nothing."
nil)
;; NOTE 2023-06-02: The `prot-common-wcag-formula' and
;; `prot-common-contrast' are taken verbatim from my `modus-themes'
;; and renamed to have the prefix `prot-common-' instead of

View file

@ -1,8 +1,8 @@
;;; prot-dired.el --- Extensions to dired.el for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2020-2026 Protesilaos
;; Copyright (C) 2020-2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -26,6 +26,11 @@
;;
;; This covers my dired.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
@ -39,90 +44,6 @@
;;;; General commands
;;;###autoload
(defun prot-dired-count-marks ()
"Print message with the number of Dired marked files."
(interactive nil dired-mode)
(if-let* ((files (dired-get-marked-files))
(length (length files))
(_ (> length 1)))
(message "%d files are marked" length)
(user-error "No files found")))
;;;###autoload
(defun prot-dired-open-directory-externally ()
"Open the current directory in an external file manager."
(interactive nil dired-mode)
(if (derived-mode-p 'dired-mode)
(call-process "xdg-open" nil 0 nil (expand-file-name default-directory))
(user-error "Only do this in a Dired buffer")))
;;;; Flat Dired listing
(defvar prot-dired-regexp-history nil
"Minibuffer history of `prot-dired-regexp-prompt'.")
(defun prot-dired-regexp-prompt ()
(let ((default (car prot-dired-regexp-history)))
(read-regexp
(format-prompt "Files matching REGEXP" default)
default 'prot-dired-regexp-history)))
(defun prot-dired--get-files (regexp)
"Return files matching REGEXP, recursively from `default-directory'."
(directory-files-recursively default-directory regexp nil))
;;;###autoload
(defun prot-dired-search-flat-list (regexp)
"Return a Dired buffer for files matching REGEXP.
Perform the search recursively from the current directory."
(interactive (list (prot-dired-regexp-prompt)))
(if-let* ((files (prot-dired--get-files regexp))
(relative-paths (mapcar #'file-relative-name files)))
(dired (cons (format "prot-flat-dired for `%s'" regexp) relative-paths))
(error "No files matching `%s'" regexp)))
(defvar prot-dired-days-prompt-history nil
"Minibuffer history for `prot-dired-days-prompt'.")
(defun prot-dired-days-prompt ()
"Prompt for days and return them as a number."
(let* ((first (car prot-dired-days-prompt-history))
(default (when (stringp first)
(string-to-number first))))
(read-number "Number of days: " default 'prot-dired-days-prompt-history)))
(defun prot-dired--get-last-modified (files days)
"Return list of FILES last modified since DAYS."
(seq-filter
(lambda (file)
(and-let* ((attributes (file-attributes file))
(last-modified (nth 5 attributes))
(last-modified-seconds (time-to-seconds last-modified))
(current-time (current-time))
(current-time-seconds (time-to-seconds current-time))
(delta-seconds (* days 24 60 60))
(oldest-seconds (- current-time-seconds delta-seconds))
(_ (>= last-modified-seconds oldest-seconds)))))
files))
;;;###autoload
(defun prot-dired-search-flat-list-since-days (regexp days)
"Return Dired buffer with files matching REGEXP up to DAYS since last modification.
Perform the search recursively from the current directory."
(interactive
(list
(prot-dired-regexp-prompt)
(prot-dired-days-prompt)))
(if-let* ((files (prot-dired--get-files regexp)))
(if-let* ((files-filtered (prot-dired--get-last-modified files days))
(relative-paths (mapcar #'file-relative-name files-filtered)))
(dired (cons (format "prot-flat-dired since %d days for `%s'" days regexp) relative-paths))
(error "No files last modified within the last %d days" days))
(error "No files matching `%s'" regexp)))
;;;; General commands
;; NOTE 2023-06-27: This user option is quick-and-dirty. I prefer not
;; to have an option at all and simply do the right thing based on
;; `dired-guess-shell-alist-user'.
@ -171,36 +92,51 @@ Restore the buffer with \\<dired-mode-map>`\\[revert-buffer]'."
(dired-do-kill-lines)
(add-to-history 'prot-dired--limit-hist regexp))
(defvar prot-dired-grep-marked-files-history nil
"Minibuffer history for `prot-dired-grep-marked-files-prompt'.")
(defun prot-dired-grep-marked-files-prompt ()
"Prompt for string to search for with `prot-dired-grep-marked-files'."
(read-string
"grep for PATTERN in marked files: "
nil 'prot-dired-grep-marked-files-history))
(defvar prot-dired--find-grep-hist '()
"Minibuffer history for `prot-dired-grep-marked-files'.")
;; Also see `prot-search-grep' from prot-search.el.
;;;###autoload
(defun prot-dired-grep-marked-files (files regexp)
"Run `find' with `grep' for REGEXP on marked Dired FILES.
If FILES contains directories, search through them recursively.
Ignore .git directories."
(defun prot-dired-grep-marked-files (regexp &optional arg)
"Run `find' with `grep' for REGEXP on marked files.
When no files are marked or when just a single one is marked,
search the entire directory instead.
With optional prefix ARG target a single marked file.
We assume that there is no point in marking a single file and
running find+grep on its contents. Visit it and call `occur' or
run grep directly on it without the whole find part."
(interactive
(if-let* ((marks (dired-get-marked-files)))
(list marks (prot-dired-grep-marked-files-prompt))
(user-error "Mark one or more files or directories")))
(compilation-start
(concat
"find " (mapconcat #'shell-quote-argument files " ")
" -path " (shell-quote-argument "*/.git") " -prune "
" -o -type f "
" -exec grep -nHER --color=auto " (shell-quote-argument regexp) " "
(shell-quote-argument "{}")
" " (shell-quote-argument "+"))
'grep-mode
(lambda (_mode) (format "*prot-dired-grep-marked for `%s'*" regexp))
t))
(list
(read-string "grep for PATTERN (marked files OR current directory): " nil 'prot-dired--find-grep-hist)
current-prefix-arg)
dired-mode)
(when-let* ((marks (dired-get-marked-files 'no-dir))
(files (mapconcat #'identity marks " "))
(args (if (or arg (length> marks 1))
;; Thanks to Sean Whitton for pointing out an
;; earlier superfluity of mine: we do not need
;; to call grep through find when we already
;; know the files we want to search in. Check
;; Sean's dotfiles:
;; <https://git.spwhitton.name/dotfiles>.
;;
;; Any other errors or omissions are my own.
(format "grep -nH --color=auto %s %s" (shell-quote-argument regexp) files)
(concat
"find . -not " (shell-quote-argument "(")
" -wholename " (shell-quote-argument "*/.git*")
" -prune " (shell-quote-argument ")")
" -type f"
" -exec grep -nHE --color=auto " regexp " "
(shell-quote-argument "{}")
" " (shell-quote-argument ";") " "))))
(compilation-start
args
'grep-mode
(lambda (mode) (format "*prot-dired-find-%s for '%s'" mode regexp))
t)))
;;;; Subdir extras and Imenu setup
@ -244,7 +180,7 @@ For more on such headings, read `dired-maybe-insert-subdir'."
(while (and (prot-dired-subdirectory-previous)
(not (equal (dired-current-directory)
(expand-file-name default-directory))))
(dired-kill-subdir)))
(dired-kill-subdir)))
(autoload 'cl-remove-if-not "cl-seq")
@ -272,10 +208,10 @@ For more on such headings, read `dired-maybe-insert-subdir'."
When items are marked, insert those which are subsirectories of
the current directory. Ignore regular files.
If no files are active and point is on a subdirectory line,
If no marks are active and point is on a subdirectory line,
insert it directly.
If no files are active and point is not on a subdirectory line,
If no marks are active and point is not on a subdirectory line,
prompt for a subdirectory using completion.
With optional ARG as a single prefix (`\\[universal-argument]')

View file

@ -1,76 +0,0 @@
;;; prot-ediff.el --- Ediff extensions for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2025-2026 Protesilaos
;; Author: Protesilaos <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Extensions for Ediff, intended for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;; Code:
(require 'ediff)
(defun prot-ediff-store-layout ()
"Store current frame window configuration as a frame parameter.
Add this function to the `ediff-before-setup-hook'.
Also see `prot-ediff-restore-layout'."
(let ((frame (selected-frame)))
(set-frame-parameter
frame
'prot-ediff-last-layout
(current-window-configuration frame))))
(defun prot-ediff-restore-layout ()
"Restore the frame's window configuration.
Add this function to the `ediff-quit-hook'.
Also see `prot-ediff-store-layout'."
(if-let* ((layout (frame-parameter (selected-frame) 'prot-ediff-last-layout)))
(set-window-configuration layout)
;; We do not signal a `user-error' here because that would prevent
;; `ediff-quit' from closing the Ediff session.
(message "No Ediff window configuration for the current frame")))
(defun prot-ediff-visible-buffers-2 ()
"Run ediff on the buffers displayed in the current frame's two windows."
(interactive)
(if-let* ((windows (window-list))
(_ (= (length windows) 2))
(buffers (mapcar #'window-buffer windows)))
(pcase-let ((`(,first ,second) buffers))
(ediff-buffers first second))
(user-error "Can only operate on two windows")))
(defun prot-ediff-visible-buffers-3 ()
"Run ediff on the buffers displayed in the current frame's three windows."
(interactive)
(if-let* ((windows (window-list))
(_ (= (length windows) 3))
(buffers (mapcar #'window-buffer windows)))
(pcase-let ((`(,first ,second ,third) buffers))
(ediff-buffers3 first second third))
(user-error "Can only operate on three windows")))
(provide 'prot-ediff)
;;; prot-ediff.el ends here

View file

@ -0,0 +1,184 @@
;;; prot-elfeed.el --- Elfeed extensions for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2021-2023 Protesilaos Stavrou
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; NOTE 2022-06-08: This is old code. There are things I would like to
;; improve.
;;
;; Extensions for Elfeed, intended for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
(eval-when-compile (require 'subr-x))
(require 'elfeed nil t)
(require 'url-util)
(require 'prot-common)
(defgroup prot-elfeed ()
"Personal extensions for Elfeed."
:group 'elfeed)
(defcustom prot-elfeed-feeds-file
(thread-last user-emacs-directory (expand-file-name "feeds.el.gpg"))
"Path to file with `elfeed-feeds'."
:type 'string
:group 'prot-elfeed)
(defcustom prot-elfeed-archives-directory "~/Documents/feeds/"
"Path to directory for storing Elfeed entries."
:type 'string
:group 'prot-elfeed)
(defcustom prot-elfeed-tag-faces nil
"Add faces for certain tags.
The tags are: critical, important, personal."
:type 'boolean
:group 'prot-elfeed)
(defcustom prot-elfeed-search-tags '(critical important personal)
"List of user-defined tags.
Used by `prot-elfeed-toggle-tag'."
:type 'list
:group 'prot-elfeed)
(defface prot-elfeed-entry-critical '((t :inherit font-lock-warning-face))
"Face for Elfeed entries tagged with `critical'.")
(defface prot-elfeed-entry-important '((t :inherit font-lock-constant-face))
"Face for Elfeed entries tagged with `important'.")
(defface prot-elfeed-entry-personal '((t :inherit font-lock-variable-name-face))
"Face for Elfeed entries tagged with `personal'.")
;;;; Utilities
;;;###autoload
(defun prot-elfeed-load-feeds ()
"Load file containing the `elfeed-feeds' list.
Add this to `elfeed-search-mode-hook'."
(let ((feeds prot-elfeed-feeds-file))
(if (file-exists-p feeds)
(load-file feeds)
(user-error "Missing feeds' file"))))
(defvar elfeed-search-face-alist)
;;;###autoload
(defun prot-elfeed-fontify-tags ()
"Expand Elfeed faces if `prot-elfeed-tag-faces' is non-nil."
(if prot-elfeed-tag-faces
(setq elfeed-search-face-alist
'((critical prot-elfeed-entry-critical)
(important prot-elfeed-entry-important)
(personal prot-elfeed-entry-personal)
(unread elfeed-search-unread-title-face)))
(setq elfeed-search-face-alist
'((unread elfeed-search-unread-title-face)))))
(defvar prot-elfeed--tag-hist '()
"History of inputs for `prot-elfeed-toggle-tag'.")
(defun prot-elfeed--character-prompt (tags)
"Helper of `prot-elfeed-toggle-tag' to read TAGS."
(let ((def (car prot-elfeed--tag-hist)))
(completing-read
(format "Toggle tag [%s]: " def)
tags nil t nil 'prot-elfeed--tag-hist def)))
(defvar elfeed-show-entry)
(declare-function elfeed-tagged-p "elfeed")
(declare-function elfeed-search-toggle-all "elfeed")
(declare-function elfeed-show-tag "elfeed")
(declare-function elfeed-show-untag "elfeed")
;;;###autoload
(defun prot-elfeed-toggle-tag (tag)
"Toggle TAG for the current item.
When the region is active in the `elfeed-search-mode' buffer, all
entries encompassed by it are affected. Otherwise the item at
point is the target. For `elfeed-show-mode', the current entry
is always the target.
The list of tags is provided by `prot-elfeed-search-tags'."
(interactive
(list
(intern
(prot-elfeed--character-prompt prot-elfeed-search-tags))))
(if (derived-mode-p 'elfeed-show-mode)
(if (elfeed-tagged-p tag elfeed-show-entry)
(elfeed-show-untag tag)
(elfeed-show-tag tag))
(elfeed-search-toggle-all tag)))
(defvar elfeed-show-truncate-long-urls)
(declare-function elfeed-entry-title "elfeed")
(declare-function elfeed-show-refresh "elfeed")
;;;; General commands
(defvar elfeed-search-filter-active)
(defvar elfeed-search-filter)
(declare-function elfeed-db-get-all-tags "elfeed")
(declare-function elfeed-search-update "elfeed")
(declare-function elfeed-search-clear-filter "elfeed")
(defun prot-elfeed--format-tags (tags sign)
"Prefix SIGN to each tag in TAGS."
(mapcar (lambda (tag)
(format "%s%s" sign tag))
tags))
;;;###autoload
(defun prot-elfeed-search-tag-filter ()
"Filter Elfeed search buffer by tags using completion.
Completion accepts multiple inputs, delimited by `crm-separator'.
Arbitrary input is also possible, but you may have to exit the
minibuffer with something like `exit-minibuffer'."
(interactive)
(unwind-protect
(elfeed-search-clear-filter)
(let* ((elfeed-search-filter-active :live)
(db-tags (elfeed-db-get-all-tags))
(plus-tags (prot-elfeed--format-tags db-tags "+"))
(minus-tags (prot-elfeed--format-tags db-tags "-"))
(all-tags (delete-dups (append plus-tags minus-tags)))
(tags (completing-read-multiple
"Apply one or more tags: "
all-tags #'prot-common-crm-exclude-selected-p t))
(input (string-join `(,elfeed-search-filter ,@tags) " ")))
(setq elfeed-search-filter input))
(elfeed-search-update :force)))
(provide 'prot-elfeed)
;;; prot-elfeed.el ends here

View file

@ -1,94 +0,0 @@
;;; prot-elisp.el --- Emacs Lisp extras for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2025-2026 Protesilaos
;; Author: Protesilaos <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Extensions for Emacs Lisp, intended for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;; Code:
;;;###autoload
(defun prot-elisp-eval-and-print-last-sexp ()
"Evaluate and print expression before point like `eval-print-last-sexp'.
Prepend a comment to the return value. Also copy the return value to
the `kill-ring' and set the mark to where point was before inserting the
return value."
(declare (interactive-only t))
(interactive)
(if-let* ((string (thing-at-point 'sexp :no-properties))
(_ (not (string-prefix-p ";" string)))
(expression (read string)))
(let ((return-value (eval expression)))
(kill-new (format "%S" return-value))
(message "Copied: `%S'" return-value)
(push-mark (point))
(insert (format "\n%S\n" return-value))
(string-insert-rectangle (+ (mark) 1) (- (point) 1) ";; => "))
(user-error "No expression at point")))
(defvar-keymap prot-elisp-macroexpand-mode-map
:doc "keymap for `prot-elisp-macroexpand-mode'."
:parent special-mode-map)
(define-derived-mode prot-elisp-macroexpand-mode emacs-lisp-mode "MacroExpand"
"Like `emacs-lisp-mode' but for macroexpanded forms."
:interactive nil
(read-only-mode 1)
(display-line-numbers-mode 1))
;;;###autoload
(defun prot-elisp-pp-macroexpand-last-sexp ()
"Like `pp-macroexpand-last-sexp' but with a generic `display-buffer'.
Now use `display-buffer-alist' like the Lisp gods intended."
(declare (interactive-only t))
(interactive)
(if-let* ((thing (thing-at-point 'sexp :no-properties))
(expression (read thing))
(buffer (get-buffer-create "*prot-elisp-macroexpand*"))
(inhibit-read-only t))
(progn
(with-current-buffer buffer
(erase-buffer)
(insert (format "%S" (macroexpand-1 expression)))
(prot-elisp-macroexpand-mode)
(pp-buffer))
(display-buffer buffer))
(user-error "No expression to macroexpand")))
;;;###autoload
(defun prot-elisp-copy-current-definition ()
"Copy the symbol of the definition around point."
(interactive)
(if-let* ((text (save-excursion
(beginning-of-defun)
(buffer-substring-no-properties (line-beginning-position) (line-end-position))))
(definition (replace-regexp-in-string "(.+?\s+\\(.+?\\)\s+.*" "\\1" text)))
(progn
(kill-new definition)
(message "Copied `%s'" definition))
(message "Cannot find definition near point")))
(provide 'prot-elisp)
;;; prot-elisp.el ends here

View file

@ -1,8 +1,8 @@
;;; prot-embark.el --- Custom Embark keymaps -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Protesilaos
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1") (embark "0.23"))
@ -114,25 +114,5 @@
"s" #'sort-lines
"u" #'untabify)
;; The minimal indicator shows cycling options, but I have no use
;; for those. I want it to be silent.
(defun prot-embark-no-minimal-indicator ())
(advice-add #'embark-minimal-indicator :override #'prot-embark-no-minimal-indicator)
(defun prot-embark-act-no-quit ()
"Call `embark-act' but do not quit after the action."
(interactive)
(let ((embark-quit-after-action nil))
(call-interactively #'embark-act)))
(defun prot-embark-act-quit ()
"Call `embark-act' and quit after the action."
(interactive)
(let ((embark-quit-after-action t))
(call-interactively #'embark-act))
(when (and (> (minibuffer-depth) 0)
(derived-mode-p 'completion-list-mode))
(abort-recursive-edit)))
(provide 'prot-embark)
;;; prot-embark.el ends here

View file

@ -0,0 +1,146 @@
;;; prot-evil.el --- Extras for Evil mode -*- lexical-binding: t -*-
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1") (evil "1.15.0"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or (at
;; your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
(require 'evil)
;;;; The "basic" state
(defvar prot-evil-basic-tag " <PB> "
"Mode line tag for the prot-basic state.")
(defvar prot-evil-basic-message "-- PROT BASIC --"
"Echo area message when entering the prot basic state.")
(evil-define-state prot-basic
"Basic Vim keys to work in most (?) read-only major modes."
:tag 'prot-evil-basic-tag
:message 'prot-evil-basic-message)
(evil-define-command evil-force-prot-basic-state ()
"Switch to state without recording current command."
:repeat abort
:suppress-operator t
(evil-prot-basic-state))
(evil-define-key 'prot-basic global-map
"0" #'evil-beginning-of-line
"$" #'evil-end-of-line
"h" #'evil-backward-char
"j" #'evil-next-line
"k" #'evil-previous-line
"l" #'evil-forward-char
"i" #'evil-insert
"v" #'evil-visual-char
"V" #'evil-visual-line)
(defun prot-evil-need-basic-p ()
"Return non-nil if the basic state should be used."
(or buffer-read-only
(memq major-mode evil-prot-basic-state-modes)))
(defun prot-evil-normal-or-basic-state ()
"Return to normal or basic state per `prot-evil-need-basic-p'."
(interactive)
(if (prot-evil-need-basic-p)
(evil-force-prot-basic-state)
(evil-force-normal-state)))
;;;; The "erase" operator
(evil-define-operator prot-evil-erase (beg end type &rest _)
"Erase text from BEG to END with TYPE.
Unlike the delete operator, do not store the erased text anywhere."
(interactive "<R><x><y>")
(when (and (memq type '(inclusive exclusive))
(not (evil-visual-state-p))
(eq 'prot-evil-erase evil-this-operator)
(save-excursion (goto-char beg) (bolp))
(save-excursion (goto-char end) (eolp))
(<= 1 (evil-count-lines beg end)))
;; Imitate Vi strangeness: if motion meets above criteria,
;; delete linewise. Not for change operator or visual state.
(let ((new-range (evil-line-expand beg end)))
(setq beg (car new-range)
end (cadr new-range)
type 'line)))
(cond
((eq type 'block)
(evil-apply-on-block #'delete-region beg end nil))
((and (eq type 'line)
(= end (point-max))
(or (= beg end)
(/= (char-before end) ?\n))
(/= beg (point-min))
(= (char-before beg) ?\n))
(delete-region (1- beg) end))
(t (delete-region beg end)))
(when (and (eq type 'line)
(called-interactively-p 'any))
(evil-first-non-blank)
(when (and (not evil-start-of-line)
evil-operator-start-col
;; Special exceptions to ever saving column:
(not (memq evil-this-motion '(evil-forward-word-begin
evil-forward-WORD-begin))))
(move-to-column evil-operator-start-col))))
;;;; Integration with `prot-prefix' (leader key)
;; The `prot-prefix' feature is loaded by the `prot-emacs-essentials'
;; module.
(defun prot-evil-prefix-or-self-insert ()
"Self-insert key or return `prot-prefix-map'.
For use as a leader key in the emacs/insert evil state when the
buffer is not writable."
(interactive)
(if (prot-evil-need-basic-p)
(set-transient-map prot-prefix-map)
(self-insert-command 1)))
;;;; Do not pollute the kill-ring in visual state
(defun prot-evil-visual-paste-no-kill (&rest args)
"Do not add visual selection to the `kill-ring' while pasting.
Add as :around advice to `evil-paste-after' and `evil-paste-before',
applying its ARGS."
(if (evil-visual-state-p)
(cl-letf (((symbol-function 'evil-yank) #'ignore))
(apply args))
(apply args)))
(advice-add #'evil-paste-after :around #'prot-evil-visual-paste-no-kill)
(advice-add #'evil-paste-before :around #'prot-evil-visual-paste-no-kill)
(provide 'prot-evil)
;;; prot-evil.el ends here

View file

@ -1,9 +1,10 @@
;;; prot-eww.el --- Extensions for EWW -*- lexical-binding: t -*-
;; Copyright (C) 2021-2026 Protesilaos
;; Copyright (C) 2021-2023 Protesilaos Stavrou, Abhiseck Paira
;; Author: Protesilaos <info@protesilaos.com>
;; Maintainer: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Abhiseck Paira <abhiseckpaira@disroot.org>
;; Maintainer: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -27,79 +28,359 @@
;;
;; Extensions for the eww, intended for my Emacs setup:
;; <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;; XXX NOTE XXX 2023-05-19: Much of this code is severely out of date.
;; I plan to review it. DO NOT USE!!!
;;; Code:
(require 'shr)
(require 'eww)
(require 'url-parse)
(require 'prot-common)
(defgroup prot-eww nil
(defgroup prot-eww ()
"Tweaks for EWW."
:group 'eww)
(defun prot-eww--get-urls ()
"Get all links in the current buffer."
(let ((links nil))
;;;; Basic setup
;; TODO 2021-10-15: Deprecate this in favour of what we added to Emacs29.
;; <https://protesilaos.com/codelog/2021-10-15-emacs-29-eww-rename-buffers/>.
(defun prot-eww--rename-buffer ()
"Rename EWW buffer using page title or URL.
To be used by `eww-after-render-hook'."
(let ((name (if (equal "" (plist-get eww-data :title))
(plist-get eww-data :url)
(plist-get eww-data :title))))
(rename-buffer (format "*%s # eww*" name) t)))
(add-hook 'eww-after-render-hook #'prot-eww--rename-buffer)
(advice-add 'eww-back-url :after #'prot-eww--rename-buffer)
(advice-add 'eww-forward-url :after #'prot-eww--rename-buffer)
;;;; History extras
(defvar prot-eww-visited-history '()
"History of visited URLs.")
(defcustom prot-eww-save-history-file
(locate-user-emacs-file "prot-eww-visited-history")
"File to save the value of `prot-eww-visited-history'."
:type 'file
:group 'prot-eww)
(defcustom prot-eww-save-visited-history nil
"Whether to save `prot-eww-visited-history'.
If non-nil, save the value of `prot-eww-visited-history' in
`prot-eww-save-history-file'."
:type 'boolean
:group 'prot-eww)
(defcustom prot-eww-list-history-buffer "*prot-eww-history*"
"Name of buffer for `prot-eww-list-history'."
:type 'string
:group 'prot-eww)
;; These history related functions are adapted from eww.
(defun prot-eww--save-visited-history ()
"Save the value of `prot-eww-visited-history' in a file.
The file is determined by the variable `prot-eww-save-history-file'."
(when prot-eww-save-visited-history
(with-temp-file prot-eww-save-history-file
(insert (concat ";; Auto-generated file;"
" don't edit -*- mode: lisp-data -*-\n"))
(pp prot-eww-visited-history (current-buffer)))))
(defun prot-eww--read-visited-history (&optional error-out)
"Read history from `prot-eww-save-history-file'.
If ERROR-OUT, signal `user-error' if there is no history."
(when prot-eww-save-visited-history
(let ((file prot-eww-save-history-file))
(setq prot-eww-visited-history
(unless (zerop
(or (file-attribute-size (file-attributes file))
0))
(with-temp-buffer
(insert-file-contents file)
(read (current-buffer)))))
(when (and error-out (not prot-eww-visited-history))
(user-error "No history is defined")))))
(unless prot-eww-visited-history
(prot-eww--read-visited-history t))
(defun prot-eww--history-prepare ()
"Prepare dedicated buffer for browsing history."
(set-buffer (get-buffer-create prot-eww-list-history-buffer))
(prot-eww-history-mode)
(let ((inhibit-read-only t)
start)
(erase-buffer)
(setq-local header-line-format
"EWW Browsing History (prot-eww)")
(dolist (history prot-eww-visited-history)
(setq start (point))
(insert (format "%s" history) "\n")
(put-text-property start (1+ start) 'prot-eww-history history))
(goto-char (point-min))))
;;;###autoload
(defun prot-eww-list-history ()
"Display `prot-eww-visited-history' in a dedicated buffer.
This is a replacement for `eww-list-histories' (or equivalent),
as it can combine URLs in the Gopher or Gemini protocols."
(interactive)
(when prot-eww-visited-history
(prot-eww--save-visited-history))
(prot-eww--read-visited-history t)
(pop-to-buffer prot-eww-list-history-buffer)
(prot-eww--history-prepare))
(defvar prot-eww-history-kill-ring nil
"Store the killed history element.")
(defun prot-eww-history-kill ()
"Kill the current history."
(interactive)
(let* ((start (line-beginning-position))
(history (get-text-property start 'prot-eww-history))
(inhibit-read-only t))
(unless history
(user-error "No history on the current line"))
(forward-line 1)
(push (buffer-substring start (point))
prot-eww-history-kill-ring)
(delete-region start (point))
(setq prot-eww-visited-history (delq history
prot-eww-visited-history))
(prot-eww--save-visited-history)))
(defun prot-eww-history-yank ()
"Yank a previously killed history to the current line."
(interactive)
(unless prot-eww-history-kill-ring
(user-error "No previously killed history"))
(beginning-of-line)
(let ((inhibit-read-only t)
(start (point))
history)
(insert (pop prot-eww-history-kill-ring))
(setq history (get-text-property start 'prot-eww-history))
(if (= start (point-min))
(push history prot-eww-visited-history)
(let ((line (count-lines start (point))))
(setcdr (nthcdr (1- line) prot-eww-visited-history)
(cons history (nthcdr line
prot-eww-visited-history)))))
(prot-eww--save-visited-history)))
(defun prot-eww-history-browse ()
"Browse the history under point."
(interactive)
(let ((history (get-text-property (line-beginning-position)
'prot-eww-history)))
(unless history
(user-error "No history on the current line"))
(quit-window)
(prot-eww history)))
(defvar prot-eww-history-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-k") 'prot-eww-history-kill)
(define-key map (kbd "C-y") 'prot-eww-history-yank)
(define-key map (kbd "<RET>") 'prot-eww-history-browse)
(easy-menu-define nil map
"Menu for `prot-eww-history-mode-map'."
'("prot-eww history"
["Exit" quit-window t]
["Browse" prot-eww-history-browse
:active (get-text-property (line-beginning-position)
'prot-eww-history)]
["Kill" prot-eww-history-kill
:active (get-text-property (line-beginning-position)
'prot-eww-history)]
["Yank" prot-eww-history-yank
:active prot-eww-history-kill-ring]))
map))
(define-derived-mode prot-eww-history-mode
special-mode
"prot-eww-history"
"Mode for listing history.
\\{prot-eww-history-mode-map}"
(buffer-disable-undo)
(setq truncate-lines t))
(defun prot-eww--record-history ()
"Store URL in `prot-eww-visited-history'.
To be used by `eww-after-render-hook'."
(let ((url (plist-get eww-data :url)))
(add-to-history 'prot-eww-visited-history url)))
(add-hook 'eww-after-render-hook #'prot-eww--record-history)
(advice-add 'eww-back-url :after #'prot-eww--record-history)
(advice-add 'eww-forward-url :after #'prot-eww--record-history)
;; Is there a better function to add this advice?
;;;; Commands
;; handler that browse-url calls.
(defun prot-eww--get-current-url ()
"Return the current-page's URL."
(when (eq major-mode 'eww-mode)
(plist-get eww-data :url)))
;;;###autoload
(defun prot-eww (url &optional arg)
"Pass URL to appropriate client.
With optional ARG, use a new buffer."
(interactive (list (browse-url-interactive-arg "URL: ") current-prefix-arg))
(eww url arg))
;;;###autoload
(defun prot-eww-browse-dwim (url &optional arg)
"Visit a URL, maybe from `eww-prompt-history', with completion.
With optional prefix ARG (\\[universal-argument]) open URL in a
new eww buffer. If URL does not look like a valid link, run a
web query using `eww-search-prefix'.
When called from an eww buffer, provide the current link as
\\<minibuffer-local-map>\\[next-history-element]."
(interactive
(let ((all-history (delete-dups
(append prot-eww-visited-history
eww-prompt-history)))
(current-url (prot-eww--get-current-url)))
(list
(completing-read "Run EWW on: " all-history
nil nil current-url 'eww-prompt-history current-url)
(prefix-numeric-value current-prefix-arg))))
(prot-eww url arg))
;; NOTE 2021-09-08: This uses the EWW-specific bookmarks, NOT those of
;; bookmark.el. Further below I provide integration with the latter,
;; meaning that we must either make this obsolete or make it work with
;; the new system.
;;;###autoload
(defun prot-eww-visit-bookmark (&optional arg)
"Visit bookmarked URL.
With optional prefix ARG (\\[universal-argument]) open URL in a
new EWW buffer."
(interactive "P")
(eww-read-bookmarks)
(let ((list (gensym)))
(dolist (bookmark eww-bookmarks)
(push (plist-get bookmark :url) list))
(if eww-bookmarks
(eww (completing-read "Visit EWW bookmark: " list)
(when arg 4))
(user-error "No bookmarks"))))
(defun prot-eww--capture-url-on-page (&optional position)
"Capture all the links on the current web page.
Return a list of strings. Strings are in the form LABEL @ URL.
When optional argument POSITION is non-nil, include position info
in the strings too, so strings take the form
LABEL @ URL ~ POSITION."
(let (links match)
(save-excursion
(goto-char (point-min))
(while (text-property-search-forward 'face 'shr-link)
(when-let* ((position (point))
(button (button-at position)))
(push
(list position
(button-label button)
(shr-url-at-point nil))
links))))
(nreverse links)))
(goto-char (point-max))
;; NOTE 2021-07-25: The first clause in the `or' is meant to
;; address a bug where if a URL is in `point-min' it does not get
;; captured.
(while (setq match (text-property-search-backward 'shr-url))
(let* ((raw-url (prop-match-value match))
(start-point-prop (prop-match-beginning match))
(end-point-prop (prop-match-end match))
(url (when (stringp raw-url)
(propertize raw-url 'face 'link)))
(label (replace-regexp-in-string "\n" " " ; NOTE 2021-07-25: newlines break completion
(buffer-substring-no-properties
start-point-prop end-point-prop)))
(point start-point-prop)
(line (line-number-at-pos point t))
(column (save-excursion (goto-char point) (current-column)))
(coordinates (propertize
(format "%d,%d (%d)" line column point)
'face 'shadow)))
(when url
(if position
(push (format "%-15s ~ %s @ %s"
coordinates label url)
links)
(push (format "%s @ %s"
label url)
links))))))
links))
(defun prot-eww-buffer-url-prompt ()
"Prompt for a url in the current buffer."
(when-let* ((link-data (prot-eww--get-urls))
(candidates (mapcar
(pcase-lambda (`(,position ,name ,_))
(format "%s %s" position name))
link-data))
(table (prot-common-completion-table-no-sort nil candidates))
(selection
(completing-read
(format-prompt "Select link in the current page" nil)
table))
(position (car (split-string selection "\t")))
(number (string-to-number position)))
(assoc number link-data)))
(defmacro prot-eww-act-visible-window (&rest body)
"Run BODY within narrowed-region.
If region is active run BODY within active region instead.
Return the value of the last form of BODY."
`(save-restriction
(if (use-region-p)
(narrow-to-region (region-beginning) (region-end))
(narrow-to-region (window-start) (window-end)))
,@body))
(defun prot-eww-visit-url-on-page (&optional new-buffer)
"Visit URL among those in the current buffer using completion.
With optional NEW-BUFFER as a prefix argument, visit the URL in a new
buffer instead of the current one."
(interactive "P" eww-mode)
(unless (derived-mode-p 'eww-mode)
(user-error "This command only works in an EWW buffer"))
(if-let* ((data (prot-eww-buffer-url-prompt))
(url (nth 2 data)))
(eww url new-buffer)
(error "Cannot find URL in data `%s'" data)))
;;;###autoload
(defun prot-eww-visit-url-on-page (&optional arg)
"Visit URL from list of links on the page using completion.
(defun prot-eww-jump-to-url-on-page ()
"Go the position of a URL among those in the current buffer."
(interactive nil eww-mode)
(unless (derived-mode-p 'eww-mode)
(user-error "This command only works in an EWW buffer"))
(if-let* ((data (prot-eww-buffer-url-prompt))
(position (car data)))
(goto-char position)
(error "Cannot position in data `%s'" data)))
With optional prefix ARG (\\[universal-argument]) open URL in a
new EWW buffer."
(interactive "P")
(when (derived-mode-p 'eww-mode)
(let* ((links (prot-eww--capture-url-on-page))
(selection (completing-read "Browse URL from page: " links nil t))
(url (replace-regexp-in-string ".*@ " "" selection)))
(eww url (when arg 4)))))
;;;###autoload
(defun prot-eww-jump-to-url-on-page (&optional arg)
"Jump to URL position on the page using completion.
When called without ARG (\\[universal-argument]) get URLs only
from the visible portion of the buffer. But when ARG is provided
consider whole buffer."
(interactive "P")
(when (derived-mode-p 'eww-mode)
(let* ((links
(if arg
(prot-eww--capture-url-on-page t)
(prot-eww-act-visible-window
(prot-eww--capture-url-on-page t))))
(prompt-scope (if arg
(propertize "URL on the page" 'face 'warning)
"visible URL"))
(prompt (format "Jump to %s: " prompt-scope))
(selection (completing-read prompt links nil t))
(position (replace-regexp-in-string "^.*(\\([0-9]+\\))[\s\t]+~" "\\1" selection))
(point (string-to-number position)))
(goto-char point))))
(defvar prot-eww--occur-feed-regexp
(concat "\\(rss\\|atom\\)\\+xml.\\(.\\|\n\\)"
".*href=[\"']\\(.*?\\)[\"']")
"Regular expression to match web feeds in HTML source.")
;;;###autoload
(defun prot-eww-find-feed ()
"Produce bespoke buffer with RSS/Atom links from XML source."
(interactive nil eww-mode)
(unless (derived-mode-p 'eww-mode)
(user-error "This command only works in an EWW buffer"))
(interactive)
(let* ((url (or (plist-get eww-data :start)
(plist-get eww-data :contents)
(plist-get eww-data :home)
@ -120,12 +401,124 @@ buffer instead of the current one."
(let ((inhibit-read-only t)
(base-url (replace-regexp-in-string "\\(.*/\\)[^/]+\\'" "\\1" url)))
(goto-char (point-min))
(while (< (point) (point-max))
(goto-char (line-beginning-position))
(when (and (looking-at prot-common-url-regexp)
(not (looking-at (format "%s.*" url))))
(insert base-url))
(forward-line 1)))))))
(unless (re-search-forward prot-common-url-regexp nil t)
(re-search-forward ".*")
(replace-match (concat base-url "\\&"))))))))
;;TODO: Add this variable as user-option, that is, define it with
;;`defcustom' so that users can use the customization interface to
;;modify it.
(defvar prot-eww-search-engines
'((debbugs . (debbugs
"https://debbugs.gnu.org/cgi/bugreport.cgi?bug="
hist-var prot-eww--debbugs-hist))
(wikipedia . (wikipedia
"https://en.m.wikipedia.org/w/index.php?search="
hist-var prot-eww--wikipedia-hist))
(archwiki . (archwiki
"https://wiki.archlinux.org/index.php?search="
hist-var prot-eww--archwiki-hist))
(aur . (aur "https://aur.archlinux.org/packages/?K="
hist-var prot-eww--aur-hist)))
"Alist of Plist of web search engines related data.
From now on refer to this type of data as APLIST. Each element
of APLIST is (KEY . VALUE) pair. KEY is a symbol specifying
search engine name. The VALUE is property list.
The plist has two key-value pairs. K1 is the same symbol has KEY
and V1 is search string of the search engine.
K2 is the symbol 'hist-var', V2 is also a symbol that has a format
'prot-eww--K1-hist'.
NOTE: If you modify this variable after `prot-eww' is loaded you
need to run the following code after modification:
(prot-eww--define-hist-var prot-eww-search-engines)")
;; Below 's-string' is short for 'search-string'. For wikipedia which
;; is this string: "https://en.m.wikipedia.org/w/index.php?search=". I
;; use this name because I don't know it's proper name.
;; Define constructor and selectors functions to access
;; `prot-eww-search-engines'.
;; the constructor
(defun prot-eww--cons-search-engines (name s-string)
"Include a new Alist element.
The alist element is added to variable `prot-eww-search-engines'.
NAME should be symbol representing the search engine. S-STRING
should be string, which is specific to named search engine."
(let ((my-plist `(,name ,s-string))
(hist-var-name (format "prot-eww--%s-hist"
(symbol-name name))))
(plist-put my-plist 'hist-var (intern hist-var-name))
(let ((my-alist (cons name my-plist)))
(add-to-list 'prot-eww-search-engines my-alist))))
;; Selectors definitions start
(defun prot-eww--select-hist-name (aplist engine-name)
"Get hist-var-name from APLIST of ENGINE-NAME."
(let ((hist-var-name (plist-get
(alist-get engine-name aplist)
'hist-var)))
hist-var-name))
(defun prot-eww--select-engine-names (aplist)
"Return a list of search-engine names from APLIST.
Each value of the list is a string."
(mapcar (lambda (x) (format "%s" (car x)))
aplist))
(defun prot-eww--select-s-string (aplist engine-name)
"Return the search-string for specified ENGINE-NAME from APLIST."
(plist-get
(alist-get engine-name aplist)
engine-name))
;; Selector definitions end here.
(defun prot-eww--define-hist-var (aplist)
"Initialize APLIST hist-variables to empty list; return nil."
(let ((engine-names
(prot-eww--select-engine-names aplist)))
(dolist (engine engine-names)
(let ((hist-var-name
(prot-eww--select-hist-name aplist
(intern engine))))
(set hist-var-name '())))))
(prot-eww--define-hist-var prot-eww-search-engines)
;;;###autoload
(defun prot-eww-search-engine (engine s-term &optional arg)
"Search S-TERM using ENGINE.
ENGINE is an assossiation defined in `prot-eww-search-engines'.
With optional prefix ARG (\\[universal-argument]) open the search
result in a new buffer."
(interactive
(let* ((engine-list (prot-eww--select-engine-names
prot-eww-search-engines))
(engine-name (completing-read
"Search with: " engine-list nil t nil
'prot-eww--engine-hist))
(history-list (prot-eww--select-hist-name
prot-eww-search-engines
(intern engine-name)))
(search-term (read-string
"Search for: " nil history-list)))
(list engine-name search-term
(prefix-numeric-value current-prefix-arg))))
(let* ((s-string
(prot-eww--select-s-string prot-eww-search-engines
(intern engine)))
(eww-pass (format "%s%s" s-string s-term))
(history-list (prot-eww--select-hist-name
prot-eww-search-engines
(intern engine))))
(add-to-history history-list s-term)
(eww eww-pass arg)))
;;;###autoload
(defun prot-eww-open-in-other-window ()
@ -134,5 +527,134 @@ buffer instead of the current one."
(other-window-prefix) ; For emacs28 -- it's a hack, but why not?
(eww-open-in-new-buffer))
;;;###autoload
(defun prot-eww-readable ()
"Use more opinionated `eww-readable'.
Set width is set to `current-fill-column'. Adjust size of
images."
(interactive)
(let ((shr-width (current-fill-column))
(shr-max-image-proportion 0.35))
(eww-readable)))
;; NOTE 2021-09-08: This uses the EWW-specific bookmarks, NOT those of
;; bookmark.el. Further below I provide integration with the latter,
;; meaning that we must either make this obsolete or make it work with
;; the new system.
;;;###autoload
(defun prot-eww-bookmark-page (title)
"Add eww bookmark named with TITLE."
(interactive
(list
(read-string "Set bookmark title: " (plist-get eww-data :title))))
(plist-put eww-data :title title)
(eww-add-bookmark))
(defvar prot-eww--punctuation-regexp "[][{}!@#$%^&*()_=+'\"?,.\|;:~`‘’“”]*"
"Regular expression of punctionation that should be removed.")
(defun prot-eww--slug-no-punct (str)
"Convert STR to a file name slug."
(replace-regexp-in-string prot-eww--punctuation-regexp "" str))
(defun prot-eww--slug-hyphenate (str)
"Replace spaces with hyphens in STR.
Also replace multiple hyphens with a single one and remove any
trailing hyphen."
(replace-regexp-in-string
"-$" ""
(replace-regexp-in-string
"-\\{2,\\}" "-"
(replace-regexp-in-string "--+\\|\s+" "-" str))))
(defun prot-eww--sluggify (str)
"Make STR an appropriate file name slug."
(downcase (prot-eww--slug-hyphenate (prot-eww--slug-no-punct str))))
;;;###autoload
(defun prot-eww-download-html (name)
"Download web page and call the file with NAME."
(interactive
(list
(prot-eww--sluggify
(read-string "Set downloaded file name: " (plist-get eww-data :title)))))
(let* ((path (thread-last eww-download-directory
(expand-file-name
(concat (format-time-string "%Y%m%d_%H%M%S") "--" name ".html"))))
(out (prot-common-shell-command-with-exit-code-and-output
"wget" "-q" (format "%s" (plist-get eww-data :url))
"-O" (format "%s" (shell-quote-argument path)))))
(if (= (car out) 0)
(message "Downloaded page at %s" path)
(message "Error downloading page: %s" (cdr out)))))
(defun prot-eww--kill-buffers-when (predicate)
"Kill buffers when PREDICATE is non-nil.
Loop through the buffer list, calling PREDICATE with each buffer.
When calling PREDICATE with a buffer returns non-nil, kill that
buffer.
PREDICATE must be function that takes buffer-object as the one
and only argument. It should return nil or non-nil."
(let ((list-buffers (buffer-list)))
(dolist (buffer list-buffers)
(when (funcall predicate buffer)
(kill-buffer buffer)))))
(defun prot-eww--kill-eww-buffers-p (buffer)
"Predicate function. Return nil or non-nil.
Take BUFFER, make it current, check if it has 'eww-mode' as the
`major-mode' or if its major mode is derived from `special-mode'
and has \"eww\" in the buffer-name. Then return non-nil."
(let ((case-fold-search t)) ; ignore case
(with-current-buffer buffer
(or (eq major-mode 'eww-mode)
(and (derived-mode-p 'special-mode)
(string-match "\\*.*eww.*\\*" (buffer-name)))))))
(defun prot-eww-kill-eww-buffers ()
"Kill all EWW buffers.
Also kill special buffers made by EWW for example buffers like
\"*eww-bookmarks*\", \"*eww-history*\" etc."
(prot-eww--kill-buffers-when 'prot-eww--kill-eww-buffers-p))
(defcustom prot-eww-delete-cookies t
"If non-nil delete cookies when `prot-eww-quit' is called."
:type 'boolean
:group 'prot-eww)
(defun prot-eww-delete-cookies ()
"Delete cookies from the cookie file."
(when prot-eww-delete-cookies
(url-cookie-delete-cookies)))
;; TODO: Make it defcustom
(defvar prot-eww-quit-hook nil
"Run this hook when `prot-eww-quit' is called.")
;; Populate the hook with these functions.
(dolist (func '(prot-eww-delete-cookies
prot-eww-kill-eww-buffers
prot-eww--save-visited-history))
(add-hook 'prot-eww-quit-hook func))
;;;###autoload
(defun prot-eww-quit ()
"Quit eww, kill all its buffers, delete all cookies.
As a final step, save `prot-eww-visited-history' to a file (see
`prot-eww-save-history-file')."
(interactive)
(if prot-eww-save-visited-history
(when (y-or-n-p "Are you sure you want to quit eww? ")
(run-hooks 'prot-eww-quit-hook))
;;
;; Now users have full control what `prot-eww-quit' does, by
;; modifying `prot-eww-quit-hook'.
(when (yes-or-no-p "Are you sure you want to quit eww?")
(run-hooks 'prot-eww-quit-hook))))
(provide 'prot-eww)
;;; prot-eww.el ends here

View file

@ -1,363 +0,0 @@
;;; prot-icons.el --- Get characters, icons, and symbols for things -*- lexical-binding: t -*-
;; Copyright (C) 2025-2026 Protesilaos
;; Author: Protesilaos <info@protesilaos.com>
;; Maintainer: Protesilaos <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or (at
;; your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Extensions to get icons and symbols for files, buffers, or related.
;; Intended for my Emacs setup: <https://protesilaos.com/emacs/dotemacs/>.
;;; Code:
(require 'prot-common)
(defgroup prot-icons nil
"Get characters, icons, and symbols for things."
:group 'convenience)
(defcustom prot-icons-style 'symbolic
"The style of the icons.
- `alphabetic' expresses icons using alphabetic characters.
- `emoji' represents icons as emoji characters.
- `symbolic' uses Unicode symbols for the icons.
Any other value has the same meaning as `text'."
:type '(choice
(const :tag "Alphabetic characters" alphabetic)
(const :tag "Emoji characters" emoji)
(const :tag "Unicode symbols" symbolic)))
(defface prot-icons-icon
'((t :inherit (bold fixed-pitch)))
"Basic attributes for an icon."
:group 'prot-icons)
(defface prot-icons-red
'((default :inherit prot-icons-icon)
(((class color) (min-colors 88) (background light))
:foreground "#aa3232")
(((class color) (min-colors 88) (background dark))
:foreground "#f06464")
(t :foreground "red"))
"Face for icons."
:group 'prot-icons)
(defface prot-icons-green
'((default :inherit prot-icons-icon)
(((class color) (min-colors 88) (background light))
:foreground "#107010")
(((class color) (min-colors 88) (background dark))
:foreground "#33bb33")
(t :foreground "green"))
"Face for icons."
:group 'prot-icons)
(defface prot-icons-yellow
'((default :inherit prot-icons-icon)
(((class color) (min-colors 88) (background light))
:foreground "#605000")
(((class color) (min-colors 88) (background dark))
:foreground "#e0a055")
(t :foreground "yellow"))
"Face for icons."
:group 'prot-icons)
(defface prot-icons-blue
'((default :inherit prot-icons-icon)
(((class color) (min-colors 88) (background light))
:foreground "#223399")
(((class color) (min-colors 88) (background dark))
:foreground "#5599ff")
(t :foreground "blue"))
"Face for icons."
:group 'prot-icons)
(defface prot-icons-magenta
'((default :inherit prot-icons-icon)
(((class color) (min-colors 88) (background light))
:foreground "#8f2270")
(((class color) (min-colors 88) (background dark))
:foreground "#ee70aa")
(t :foreground "magenta"))
"Face for icons."
:group 'prot-icons)
(defface prot-icons-cyan
'((default :inherit prot-icons-icon)
(((class color) (min-colors 88) (background light))
:foreground "#226067")
(((class color) (min-colors 88) (background dark))
:foreground "#77b0c0")
(t :foreground "cyan"))
"Face for icons."
:group 'prot-icons)
(defface prot-icons-gray
'((default :inherit prot-icons-icon)
(((class color) (min-colors 88) (background light))
:foreground "gray30")
(((class color) (min-colors 88) (background dark))
:foreground "gray70")
(t :foreground "gray"))
"Face for icons."
:group 'prot-icons)
(defface prot-icons-directory
'((t :inherit (prot-icons-icon dired-directory)))
"Face for icons."
:group 'prot-icons)
(defvar prot-icons-alphabetic
'((dired-mode "|*" prot-icons-directory)
(archive-mode "|@" prot-icons-directory)
(diff-mode "" prot-icons-yellow) ; διαφορά
(prog-mode "" prot-icons-magenta) ; πρόγραμμα
(conf-mode "" prot-icons-gray) ; πρόγραμμα
(text-mode ">Α" prot-icons-green) ; αλφάβητο
(comint-mode ">_" prot-icons-gray)
(read-only "" prot-icons-red)
(document "" prot-icons-red) ; σύγγραμμα
(audio ">Η" prot-icons-cyan) ; ήχος
(image ">Ε" prot-icons-yellow) ; εικόνα
(video ">Κ" prot-icons-blue) ; κίνηση (κινηματογράφος)
(frame "[]" prot-icons-gray)
(git "|-" prot-icons-gray)
(t ">." prot-icons-gray))
"Major modes or concepts and their corresponding icons.
Each element is a cons cell of the form (THING STRING FACE), where THING
is a symbol STRING is one or more characters that represent THING, and
FACE is the face to use for it, where applicable.")
(defvar prot-icons-emoji
'((dired-mode "📁" prot-icons-directory)
(archive-mode "💼" prot-icons-directory)
(diff-mode "🟰" prot-icons-yellow)
(prog-mode "🧬" prot-icons-magenta)
(conf-mode "📝" prot-icons-gray)
(text-mode "💬" prot-icons-green)
(comint-mode "🔮" prot-icons-gray)
(read-only "🔒" prot-icons-red)
(document "📜" prot-icons-red)
(audio "🔊" prot-icons-cyan)
(image "🎨" prot-icons-yellow)
(video "📹" prot-icons-blue)
(frame "🏠" prot-icons-gray)
(git "🪾" prot-icons-gray)
(t "🔸" prot-icons-gray))
"Same as `prot-icons-alphabetic' with emoji characters.")
(defvar prot-icons-symbolic
'((dired-mode "🖿" prot-icons-directory)
(archive-mode "🮯" prot-icons-directory)
(diff-mode "" prot-icons-yellow)
(prog-mode "" prot-icons-magenta)
(conf-mode "🗅" prot-icons-gray)
(text-mode "" prot-icons-green)
(comint-mode "" prot-icons-gray)
(read-only "" prot-icons-red)
(document "🗎" prot-icons-red)
(audio "𝅘𝅥𝅮" prot-icons-cyan)
(image "𜷻" prot-icons-yellow)
(video "" prot-icons-blue)
(frame "𜲋" prot-icons-gray)
(git "" prot-icons-gray)
(t "·" prot-icons-gray))
"Same as `prot-icons-alphabetic' with Unicode symbols.")
(defun prot-icons--get-style ()
"Return icons corresponding to `prot-icons-style'.
More specifically, return the value of `prot-icons-alphabetic' or
`prot-icons-emoji'."
(pcase prot-icons-style
('emoji prot-icons-emoji)
('symbolic prot-icons-symbolic)
(_ prot-icons-alphabetic)))
(defun prot-icons--get (thing)
"Return `prot-icons-alphabetic' representation of THING."
(unless (symbolp thing)
(error "The thing `%s' is not a symbol" thing))
(let ((icons (prot-icons--get-style)))
(when (string-suffix-p "-mode" (symbol-name thing))
(while-let ((parent (get thing 'derived-mode-parent)))
(setq thing parent)))
(or (alist-get thing icons)
(alist-get t icons))))
(defun prot-icons-get-icon (thing &optional face)
"Return propertized icon THING."
(pcase-let* ((`(,icon ,inherent-face) (prot-icons--get thing))
(face (or face inherent-face)))
(format "%2s" (propertize icon 'font-lock-face face 'face face))))
(defun prot-icons-get-file-icon (file)
"Return FILE icon and face."
(cond
((null file)
(prot-icons-get-icon nil))
((string-suffix-p "/" file)
(prot-icons-get-icon 'dired-mode))
((string-match-p (prot-common--get-file-type-regexp 'archive) file)
(prot-icons-get-icon 'archive-mode))
((string-match-p (prot-common--get-file-type-regexp 'text) file)
(prot-icons-get-icon 'text-mode))
((string-match-p (prot-common--get-file-type-regexp 'image) file)
(prot-icons-get-icon 'image))
((string-match-p (prot-common--get-file-type-regexp 'audio) file)
(prot-icons-get-icon 'audio))
((string-match-p (prot-common--get-file-type-regexp 'video) file)
(prot-icons-get-icon 'video))
((string-match-p (prot-common--get-file-type-regexp 'document) file)
(prot-icons-get-icon 'document))
((string-match-p (prot-common--get-file-type-regexp 'diff) file)
(prot-icons-get-icon 'diff-mode))
((string-match-p (prot-common--get-file-type-regexp 'program) file)
(prot-icons-get-icon 'prog-mode))
((string-match-p (prot-common--get-file-type-regexp 'program-data) file)
(prot-icons-get-icon 'conf-mode))
(t (prot-icons-get-icon nil))))
;;;; Icons for Dired
;; Adapted from `nerd-icons-dired'
(defun prot-icons-dired--add-overlay (pos string)
"Add overlay to display STRING at POS."
(let ((overlay (make-overlay (1- pos) pos)))
(overlay-put overlay 'prot-icons-overlay t)
(overlay-put overlay 'evaporate t)
(overlay-put overlay 'before-string (propertize string 'display string))
(overlay-put overlay 'after-string (propertize " " 'display '(space :align-to 1)))))
(defun prot-icons-dired--remove-all-overlays ()
"Remove all `prot-icons' overlays."
(dolist (buffer (buffer-list))
(when (and (derived-mode-p 'dired-mode) prot-icons-dired-mode)
(save-restriction
(widen)
(remove-overlays nil nil 'prot-icons-overlay t)))))
(defun prot-icons-dired--annotate ()
"Add icons to all files in the visible region of the buffer."
(save-excursion
(goto-char (point-min))
(while (and (dired-next-line 1) (not (eobp)))
(when-let* ((file (dired-get-filename nil :no-error))
(icon (if (file-directory-p file)
(prot-icons-get-file-icon (concat file "/"))
(prot-icons-get-file-icon file))))
(prot-icons-dired--add-overlay (dired-move-to-filename) icon)))))
(defun prot-icons-dired--refresh (&rest _)
"Update the display of icons of files in a Dired buffer."
(prot-icons-dired--remove-all-overlays)
(save-restriction
(widen)
(prot-icons-dired--annotate)))
(defun prot-icons-dired--setup ()
"Set up Dired to display icons."
(setq-local tab-width 1)
(prot-icons-dired--refresh))
;;;###autoload
(define-minor-mode prot-icons-dired-mode
"Display icons for Dired entries."
:global t
(if prot-icons-dired-mode
(progn
(add-hook 'dired-mode-hook #'prot-icons-dired--setup)
(add-hook 'dired-after-readin-hook 'prot-icons-dired--annotate)
(advice-add #'dired-do-redisplay :after #'prot-icons-dired--refresh)
(advice-add #'wdired-abort-changes :after #'prot-icons-dired--refresh))
(prot-icons-dired--remove-all-overlays)
(remove-hook 'dired-mode-hook #'prot-icons-dired--setup)
(remove-hook 'dired-after-readin-hook 'prot-icons-dired--annotate)
(advice-remove #'dired-do-redisplay #'prot-icons-dired--refresh)
(advice-remove #'wdired-abort-changes #'prot-icons-dired--refresh)))
;;;; Icons for Xref
;; Adapted from `nerd-icons-xref'
(defun prot-icons-xref--add-overlay (position string)
"Add overlay at POSITION to display STRING."
(let ((overlay (make-overlay position (+ position 1))))
(overlay-put overlay 'prot-icons-overlay t)
(overlay-put overlay 'evaporate t)
(overlay-put overlay 'before-string (format "%s " (propertize string 'display string)))))
(defun prot-icons-xref--add-icons ()
"Add icons to Xref headings."
(save-excursion
(goto-char (point-min))
(let ((prop))
(while (setq prop (text-property-search-forward 'xref-group))
(when-let* ((start (prop-match-beginning prop))
(end (prop-match-end prop))
(file (string-chop-newline (buffer-substring-no-properties start end)))
(icon (prot-icons-get-file-icon file)))
(prot-icons-xref--add-overlay start icon))))))
;;;###autoload
(define-minor-mode prot-icons-xref-mode
"Display icons for Xref headings."
:global t
(if prot-icons-xref-mode
(add-hook 'xref-after-update-hook #'prot-icons-xref--add-icons)
(remove-hook 'xref-after-update-hook #'prot-icons-xref--add-icons)))
;;;; Icons for Buffer menu
(defun prot-icons-buffer-menu--add-overlay (position string)
"Add overlay at POSITION to display STRING."
(let ((overlay (make-overlay position (+ position 1))))
(overlay-put overlay 'prot-icons-overlay t)
(overlay-put overlay 'evaporate t)
(overlay-put overlay 'before-string (propertize string 'display string))))
(defun prot-icons-buffer-menu--add-icons (&rest _)
"Add icons to `Buffer-menu-mode' entries."
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when (derived-mode-p 'Buffer-menu-mode)
(save-excursion
(goto-char (point-min))
(while-let ((match (text-property-search-forward 'tabulated-list-id))
(buffer (prop-match-value match))
(mode (with-current-buffer buffer major-mode))
(icon (prot-icons-get-icon mode)))
(prot-icons-buffer-menu--add-overlay (line-beginning-position) (concat icon " "))))))))
;;;###autoload
(define-minor-mode prot-icons-buffer-menu-mode
"Display icons for `Buffer-menu-mode' entries."
:global t
(if prot-icons-buffer-menu-mode
(advice-add #'list-buffers--refresh :after #'prot-icons-buffer-menu--add-icons)
(advice-remove #'list-buffers--refresh #'prot-icons-buffer-menu--add-icons)))
(provide 'prot-icons)
;;; prot-icons.el ends here

View file

@ -1,8 +1,8 @@
;;; prot-marginalia.el --- Code for my custom mode line -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Protesilaos
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -56,7 +56,7 @@
(defun prot-marginalia-buffer (buffer)
"Annotate BUFFER with the return value of function `buffer-file-name'."
(if-let* ((name (buffer-file-name (get-buffer buffer))))
(if-let ((name (buffer-file-name (get-buffer buffer))))
(prot-marginalia-display (abbreviate-file-name name))
(prot-marginalia-display (format "%s" (buffer-local-value 'major-mode (get-buffer buffer))))))
@ -66,7 +66,7 @@
(pkg (intern-soft package))
(desc (or (when (package-desc-p pkg) pkg)
(car (alist-get pkg pkg-alist))
(if-let* ((built-in (assq pkg package--builtins)))
(if-let (built-in (assq pkg package--builtins))
(package--from-builtin built-in)
(car (alist-get pkg package-archive-contents))))))
(prot-marginalia-display (package-desc-summary desc))))
@ -81,15 +81,15 @@
(defun prot-marginalia--first-line-documentation (symbol)
"Return first line of SYMBOL documentation string."
(when-let* ((doc-string (prot-marginalia--get-symbol-doc symbol))
((stringp doc-string))
((not (string-empty-p doc-string))))
(when-let ((doc-string (prot-marginalia--get-symbol-doc symbol))
((stringp doc-string))
((not (string-empty-p doc-string))))
(car (split-string doc-string "[?!.\n]"))))
(defun prot-marginalia-symbol (symbol)
"Annotate SYMBOL with its documentation string."
(when-let* ((sym (intern-soft symbol))
(doc-string (prot-marginalia--first-line-documentation sym)))
(when-let ((sym (intern-soft symbol))
(doc-string (prot-marginalia--first-line-documentation sym)))
(prot-marginalia-display doc-string)))
(provide 'prot-marginalia)

View file

@ -1,411 +0,0 @@
;;; prot-minibuffer.el --- Extensions for the minibuffer and completions -*- lexical-binding: t -*-
;; Copyright (C) 2025-2026 Protesilaos
;; Author: Protesilaos <info@protesilaos.com>
;; Maintainer: Protesilaos <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or (at
;; your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Extensions for the minibuffer and completions, intended for my
;; Emacs setup: <https://protesilaos.com/emacs/dotemacs/>.
;;; Code:
(require 'prot-icons)
(defgroup prot-minibuffer nil
"Extensions for the minibuffer and completions."
:group 'minibuffer)
;;;; Completion category grouping, sorting, and affixating
;; Add some missing completion categories to let me configure the
;; relevant prompts via the `completion-category-overrides'.
(defun prot-minibuffer@read-from-kill-ring (&rest args)
(let ((completion-extra-properties (list :category 'prot-minibuffer-kill-ring)))
(apply args)))
(defun prot-minibuffer@read-library-name (&rest args)
(let ((completion-extra-properties (list :category 'prot-minibuffer-library)))
(apply args)))
(defun prot-minibuffer@emoji--read-emoji (&rest args)
(let ((completion-extra-properties (list :category 'prot-minibuffer-emoji)))
(apply args)))
(defun prot-minibuffer@password-store--completing-read (&rest args)
(let ((completion-extra-properties (list :category 'prot-minibuffer-pass)))
(apply args)))
(defun prot-minibuffer@read-input-method-name (&rest args)
(let ((completion-extra-properties (list :category 'prot-minibuffer-input-method)))
(apply args)))
(defun prot-minibuffer--do-advice (advice functions place)
"Handle the advice installed by the function `prot-minibuffer-missing-categories-mode'.
For each function in FUNCTIONS apply ARGS."
(dolist (original functions)
(when-let* ((my-function-name (format "prot-minibuffer@%s" original))
(my-function-symbol (intern-soft my-function-name))
(args (if place
(list advice original place my-function-symbol)
(list advice original my-function-symbol))))
(apply args))))
(defvar prot-minibuffer-missing-completion-categories-symbols
'( read-from-kill-ring read-library-name emoji--read-emoji
read-input-method-name password-store--completing-read)
"Symbols to be advised by `prot-minibuffer-missing-categories-mode'.")
;;;###autoload
(define-minor-mode prot-minibuffer-missing-categories-mode
"When enabled, add missing compleiton categories to relevant prompts."
:global t
(when-let* ((functions prot-minibuffer-missing-completion-categories-symbols))
(if prot-minibuffer-missing-categories-mode
(prot-minibuffer--do-advice #'advice-add functions :around)
(prot-minibuffer--do-advice #'advice-remove functions nil))))
(defun prot-minibuffer-file-sort (files)
"Sort FILES to have directories first and the rest alphabetically.
Omit the .. directory from FILES."
(setq files (delete "../" files))
(setq files (minibuffer-sort-alphabetically files))
(let ((directory-p (lambda (file) (string-suffix-p "/" file))))
(nconc (seq-filter directory-p files)
(seq-remove directory-p files))))
(defun prot-minibuffer-file-affixate (files)
"Return FILES with prefix and suffix."
(mapcar
(lambda (file)
(list file (format "%s " (prot-icons-get-file-icon file)) ""))
files))
(defun prot-minibuffer-file-group (file transform)
"Return FILE group name unless TRANSFORM is non-nil."
(cond
(transform file)
((string-suffix-p "/" file) "Directories")
((string-prefix-p "." file) "Dotfiles")
((when-let* ((extension (file-name-extension file :include-dot))
(_ (not (string-blank-p extension))))
extension))
(t "Other files")))
(defun prot-minibuffer--set-default-sort (candidates)
"Sort CANDIDATES according to `completions-sort' and return the sorted list."
(setq candidates
(pcase completions-sort
('nil candidates)
('alphabetical (minibuffer-sort-alphabetically candidates))
('historical (minibuffer-sort-by-history candidates))
(_ (funcall completions-sort candidates)))))
(defun prot-minibuffer-symbol-sort (symbols)
"Sort SYMBOLS so that public ones come first."
(setq symbols (prot-minibuffer--set-default-sort symbols))
(let ((private-p (lambda (symbol) (string-match-p "--" symbol))))
(nconc (seq-remove private-p symbols)
(seq-filter private-p symbols))))
(defun prot-minibuffer-symbol-group (symbol-name transform)
"Return SYMBOL-NAME group unless TRANSFORM is non-nil."
(let ((first-word-fn (lambda (string)
(if (string-match "\\(.*?\\)[@/-].*" string)
(match-string 1 string)
string))))
(cond
(transform symbol-name)
((funcall first-word-fn symbol-name)))))
(defun prot-minibuffer--propertize-suffix-with-space (string)
"Propertize STRING with spacing before it."
(format " %s%s"
(if (and (or (eq completions-format 'horizontal)
(eq completions-format 'vertical))
(not prot-emacs-completion-ui))
" "
(propertize " " 'display '(space :align-to 60)))
(propertize string 'face 'completions-annotations)))
(defun prot-minibuffer-buffer-sort (buffers)
"Sort BUFFERS by visibility.
This is a copy of `beframe-buffer-sort-visibility', from my `beframe'
package."
(let* ((bufs (seq-group-by
(lambda (buf)
(cond
((eq buf (current-buffer)) :current)
((get-buffer-window buf 'visible) :visible)
(t :hidden)))
buffers))
(hidden (alist-get :hidden bufs))
(visible (alist-get :visible bufs))
(current (alist-get :current bufs)))
(nconc hidden visible current)))
;; NOTE 2026-07-25: I am disabling this because it messes up with the sorting.
;; (defun prot-minibuffer-buffer-group (buffer-name transform)
;; "Return BUFFER-NAME group name unless TRANSFORM is non-nil."
;; (cond
;; (transform buffer-name)
;; ((string-prefix-p "*" buffer-name) "Special")
;; ((string-match-p "\\`magit.*?:" buffer-name) "Git")
;; ((if-let* ((buffer (get-buffer buffer-name)))
;; (with-current-buffer buffer
;; (cond
;; ((derived-mode-p 'dired-mode)
;; "Directory")
;; ((derived-mode-p 'prog-mode)
;; "Program")
;; ((derived-mode-p 'text-mode)
;; "Prose")
;; (t
;; (format "%s" major-mode))))
;; ""))))
(defun prot-minibuffer-buffer-affixate (buffers)
"Return BUFFERS with prefix and suffix."
(mapcar
(lambda (buffer)
(let* ((buffer-object (get-buffer buffer))
(mode (with-current-buffer buffer-object major-mode)))
(list
buffer
(format "%s " (prot-icons-get-icon mode))
(prot-minibuffer--propertize-suffix-with-space (format "%s" mode)))))
buffers))
(defun prot-minibuffer-bookmark-affixate (bookmarks)
"Return BOOKMARKS with prefix and suffix."
(mapcar
(lambda (bookmark)
(let* ((data (bookmark-get-bookmark bookmark))
(file (bookmark-prop-get data 'filename)))
(list
bookmark
(format "%s " (prot-icons-get-file-icon file))
(prot-minibuffer--propertize-suffix-with-space (format "%s" file)))))
bookmarks))
(defun prot-minibuffer-library-sort (libraries)
"Sort LIBRARIES, omitting autoloads and bytecode files."
(setq libraries (seq-remove
(lambda (library)
(string-match-p "\\(-autoload\\|\\.elc\\|\\.dir-locals\\)" library))
libraries))
(prot-minibuffer--set-default-sort libraries))
(defcustom prot-minibuffer-used-input-methods
'("greek" "french-postfix" "spanish-postfix")
"Input methods that I use."
:type '(choice (const nil)
(repeat mule-input-method-string)))
(defun prot-minibuffer--my-method-p (input-method)
"Return non-nil if INPUT-METHOD is a member of `prot-minibuffer-used-input-methods'."
(member input-method prot-minibuffer-used-input-methods))
(defun prot-minibuffer-input-method-sort (input-methods)
"Sort INPUT-METHODS into `prot-minibuffer-used-input-methods' and the rest."
(setq input-methods (prot-minibuffer--set-default-sort input-methods))
(nconc (seq-filter #'prot-minibuffer--my-method-p input-methods)
(seq-remove #'prot-minibuffer--my-method-p input-methods)))
(defun prot-minibuffer-input-method-group (input-method transform)
"Group INPUT-METHOD into `prot-minibuffer-used-input-methods' and the rest.
Do it when TRANSFORM is non-nil, else return INPUT-METHOD."
(cond
(transform input-method)
((prot-minibuffer--my-method-p input-method) "My input methods")
(t "Other input methods")))
;; NOTE 2025-12-19: Maybe there is a better way, but this is okay to start with.
(defun prot-minibuffer-library-annotate (library)
"Return the group documentation of LIBRARY."
(when-let* ((group-documentation (get (intern-soft library) 'group-documentation)))
(prot-minibuffer--propertize-suffix-with-space group-documentation)))
(defun prot-minibuffer-command-annotate (command)
"Annotate COMMAND with its key binding and shortened documentation string."
(let* ((symbol (intern-soft command))
(key (if-let* ((binding (where-is-internal symbol overriding-local-map t))
(description (key-description binding))
(_ (and binding (not (stringp binding)))))
(format " %s " (propertize description 'face 'help-key-binding))
""))
(doc (if-let* ((doc (condition-case nil
(documentation symbol)
(error nil)))
(first-line (substring doc 0 (string-search "\n" doc))))
(propertize first-line 'face 'completions-annotations)
"")))
(format "%s%s" key (prot-minibuffer--propertize-suffix-with-space doc))))
;;;; Completions
(defun prot-minibuffer-completions-tweak-style ()
"Tweak the style of the Completions buffer."
(setq-local mode-line-format nil)
(setq-local cursor-in-non-selected-windows nil)
(when (and completions-header-format
(not (string-blank-p completions-header-format)))
(setq-local display-line-numbers-offset -1))
(display-line-numbers-mode 1))
(defun prot-minibuffer-quit-completions ()
"Always quit the Completions window."
(when-let* ((window (get-buffer-window "*Completions*")))
(quit-window nil window)))
(defun prot-minibuffer-choose-completion-no-exit ()
"Call `choose-completion' without exiting the minibuffer.
Also see `prot-minibuffer-choose-completion-exit' and
`prot-minibuffer-choose-completion-dwim'."
(interactive)
(if minibuffer-visible-completions
(minibuffer-choose-completion :no-exit :no-quit)
(choose-completion nil :no-exit :no-quit)
(switch-to-minibuffer)))
(defun prot-minibuffer-choose-completion-exit ()
"Call `choose-completion' and exit the minibuffer.
Also see `prot-minibuffer-choose-completion-no-exit' and
`prot-minibuffer-choose-completion-dwim'."
(interactive)
(if minibuffer-visible-completions
(or (minibuffer-choose-completion-or-exit) (exit-minibuffer))
(choose-completion nil :no-exit)
(exit-minibuffer)))
(defun prot-minibuffer-crm-p ()
"Return non-nil if `completing-read-multiple' is in use."
(when-let* ((_ (featurep 'crm))
(window (active-minibuffer-window))
(buffer (window-buffer window)))
(buffer-local-value 'crm-completion-table buffer)))
(defun prot-minibuffer-empty-p ()
"Return non-nil if the minibuffer is empty."
(eq (minibuffer-prompt-end) (point-max)))
(defun prot-minibuffer-choose-completion-dwim ()
"Call `choose-completion' that exits only on a unique match.
If the match is not unique, then complete up to the largest common
prefix or, anyhow, continue with the completion (e.g. in `find-file'
switch into the directory and then show the files therein).
Also see `prot-minibuffer-choose-completion-no-exit' and
`prot-minibuffer-choose-completion-exit'."
(interactive)
(if (prot-minibuffer-crm-p)
(if (prot-minibuffer-empty-p)
(prot-minibuffer-choose-completion-no-exit)
(prot-minibuffer-choose-completion-exit))
(choose-completion nil :no-exit :no-quit)
(switch-to-minibuffer)
(minibuffer-completion-help)
(unless (get-buffer-window "*Completions*")
(exit-minibuffer))))
(define-advice minibuffer-completion-help (:around (&rest args) prot)
"Make `minibuffer-completion-help' display *Completions* in a side window.
Make the window be at slot 0, such that the *Help* buffer produced by
`prot-minibuffer-completions-describe-at-point' is to its right."
(let ((display-buffer-overriding-action
`((display-buffer-reuse-mode-window display-buffer-in-side-window)
(mode . completion-list-mode)
(side . bottom)
(slot . 0))))
(apply args)))
(defun prot-minibuffer-completions-describe-at-point (symbol)
"Describe SYMBOL at point inside the *Completions* buffer.
Place the *Help* buffer in a side window, situated to the right of the
*Completions* buffer. Make the window have the `prot-minibuffer-help'
property, such that it can be found by `prot-minibuffer-completions-close-help'."
(interactive
(list
(intern-soft
(if minibuffer-visible-completions
(completion--selected-candidate)
(thing-at-point 'symbol)))))
(when symbol
(let ((help-window-select nil)
(display-buffer-overriding-action
`((display-buffer-reuse-mode-window display-buffer-in-side-window)
(mode . help-mode)
(side . bottom)
(slot . 1)
(window-height . fit-window-to-buffer)
(window-parameters . ((prot-minibuffer-help . t))))))
(describe-symbol symbol))))
(defun prot-minibuffer-completions-close-help ()
"Close the window that has a `prot-minibuffer-help' parameter."
(when-let* ((help (seq-find
(lambda (window)
(window-parameter window 'prot-minibuffer-help))
(window-list))))
(delete-window help)))
;;;###autoload
(define-minor-mode prot-minibuffer-completions-mode
"Tweak the interface of the minibuffer and the *Completions*."
:global t
(if prot-minibuffer-completions-mode
(progn
(setq completion-show-help nil)
(setq completion-show-inline-help nil)
(setq completions-detailed t)
(setq completions-format 'one-column)
(setq completions-header-format "")
(setq completions-max-height 12)
(setq completions-sort 'historical)
(setq completion-auto-help t)
(setq completion-auto-select nil)
(setq minibuffer-visible-completions t)
(setq completion-eager-display 'auto)
(setq completion-eager-update 'auto)
(add-hook 'completion-list-mode-hook #'prot-minibuffer-completions-tweak-style)
(add-hook 'minibuffer-exit-hook #'prot-minibuffer-quit-completions)
(add-hook 'minibuffer-exit-hook #'prot-minibuffer-completions-close-help))
(setq completion-show-help t)
(setq completion-show-inline-help nil)
(setq completions-detailed nil)
(setq completions-format 'horizontal)
(setq completions-header-format (propertize "%s possible completions:\n" 'face 'shadow))
(setq completions-max-height nil)
(setq completions-sort 'alphabetical)
(setq completion-auto-help t)
(setq completion-auto-select nil)
(setq minibuffer-visible-completions nil)
(setq completion-eager-display 'auto)
(setq completion-eager-update 'auto)
(remove-hook 'completion-list-mode-hook #'prot-minibuffer-completions-tweak-style)
(remove-hook 'minibuffer-exit-hook #'prot-minibuffer-quit-completions)
(remove-hook 'minibuffer-exit-hook #'prot-minibuffer-completions-close-help)))
(provide 'prot-minibuffer)
;;; prot-minibuffer.el ends here

View file

@ -1,8 +1,8 @@
;;; prot-modeline.el --- Code for my custom mode line -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Protesilaos
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -23,11 +23,15 @@
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
(require 'prot-common)
(require 'prot-icons)
(defgroup prot-modeline nil
"Custom modeline that is stylistically close to the default."
@ -49,11 +53,6 @@ Modify this face to, for example, add a :box attribute to all
relevant indicators (combines nicely with my `spacious-padding'
package).")
(defface prot-modeline-indicator-small
'((t :height 0.8))
"Face for modeline indicators (e.g. see my `notmuch-indicator')."
:group 'prot-modeline-faces)
(defface prot-modeline-indicator-red
'((default :inherit bold)
(((class color) (min-colors 88) (background light))
@ -174,67 +173,34 @@ package).")
"Face for modeline indicators with a background."
:group 'prot-modeline-faces)
(defface prot-modeline-indicator-gray
'((t :inherit (bold shadow)))
"Face for modeline indicators (e.g. see my `notmuch-indicator')."
:group 'prot-modeline-faces)
(defface prot-modeline-indicator-gray-bg
'((default :inherit (bold prot-modeline-indicator-button))
(((class color) (min-colors 88) (background light))
:background "#808080" :foreground "white")
(((class color) (min-colors 88) (background dark))
:background "#a0a0a0" :foreground "black")
(t :inverse-video t))
"Face for modeline indicatovrs with a background."
:group 'prot-modeline-faces)
;;;; Common helper functions
(defun prot-modeline--string-truncate-p (str)
"Return non-nil if STR should be truncated."
(cond
((or (not (stringp str))
(string-empty-p str)
(string-blank-p str))
nil)
((and (prot-common-window-narrow-p)
(> (length str) prot-modeline-string-truncate-length)
(not (one-window-p :no-minibuffer))))))
(and (prot-common-window-small-p)
(> (length str) prot-modeline-string-truncate-length)
(not (one-window-p :no-minibuffer))))
(defun prot-modeline--truncate-p ()
"Return non-nil if truncation should happen.
This is a more general and less stringent variant of
`prot-modeline--string-truncate-p'."
(and (prot-common-window-narrow-p)
(and (prot-common-window-small-p)
(not (one-window-p :no-minibuffer))))
(defun prot-modeline-string-cut-end (str)
(defun prot-modeline-string-truncate (str)
"Return truncated STR, if appropriate, else return STR.
Cut off the end of STR by counting from its start up to
`prot-modeline-string-truncate-length'."
Truncation is done up to `prot-modeline-string-truncate-length'."
(if (prot-modeline--string-truncate-p str)
(concat (substring str 0 prot-modeline-string-truncate-length) "...")
str))
(defun prot-modeline-string-cut-beginning (str)
"Return truncated STR, if appropriate, else return STR.
Cut off the beginning of STR by counting from its end up to
`prot-modeline-string-truncate-length'."
(defun prot-modeline-string-truncate-end (str)
"Like `prot-modeline-string-truncate' but truncate from STR beginning."
(if (prot-modeline--string-truncate-p str)
(concat "..." (substring str (- prot-modeline-string-truncate-length)))
str))
(defun prot-modeline-string-cut-middle (str)
"Return truncated STR, if appropriate, else return STR.
Cut off the middle of STR by counting half of
`prot-modeline-string-truncate-length' both from its beginning
and end."
(let ((half (floor prot-modeline-string-truncate-length 2)))
(if (prot-modeline--string-truncate-p str)
(concat (substring str 0 half) "..." (substring str (- half)))
str)))
(defun prot-modeline--first-char (str)
"Return first character from STR."
(substring str 0 1))
@ -275,16 +241,15 @@ Specific to the current window's mode line.")
'(:eval
(when (and (mode-line-window-selected-p)
(buffer-narrowed-p)
(not (memq major-mode '(Info-mode help-mode message-mode special-mode))))
(not (derived-mode-p 'Info-mode 'help-mode 'special-mode 'message-mode)))
(propertize " Narrow " 'face 'prot-modeline-indicator-cyan-bg)))
"Mode line construct to report the narrowed state of the current buffer.")
"Mode line construct to report the multilingual environment.")
;;;; Input method
(defvar-local prot-modeline-input-method
'(:eval
(when (and (mode-line-window-selected-p)
current-input-method-title)
(when current-input-method-title
(propertize (format " %s " current-input-method-title)
'face 'prot-modeline-indicator-green-bg
'mouse-face 'mode-line-highlight)))
@ -302,15 +267,76 @@ Specific to the current window's mode line.")
'mouse-face 'mode-line-highlight)))
"Mode line construct for showing remote file name.")
;;;; Dedicated window
;;;; Evil state
(defvar-local prot-modeline-window-dedicated-status
(defvar evil-state)
(defvar evil-visual-selection)
(defconst prot-modeline-evil-state-tags
'((normal :short "<N>" :long "NORMAL")
(insert :short "<I>" :long "INSERT")
(visual :short "<V>" :long "VISUAL")
(vblock :short "<Vb>" :long "VBLOCK")
(vline :short "<Vl>" :long "VLINE")
(vsline :short "<Vsl>" :long "VSLINE")
(motion :short "<M>" :long "MOTION")
(emacs :short "<E>" :long "EMACS")
(operator :short "<O>" :long "OPERATE")
(replace :short "<R>" :long "REPLACE")
(prot-basic :short "<B>" :long "BASIC"))
"Short and long tags for Evil states.")
(defun prot-modeline--evil-get-tag (state variant)
"Get Evil STATE tag of VARIANT :short or :long.
VARIANT of the state tag is either :short or :long, as defined in
`prot-modeline-evil-state-tags'."
(let ((tags (alist-get state prot-modeline-evil-state-tags)))
(plist-get tags (or variant :short))))
(defun prot-modeline--evil-get-format-specifier (variant)
"Return a `format' specifier for VARIANT.
VARIANT of the state tag is either :short or :long, as defined in
`prot-modeline-evil-state-tags'."
(if (eq variant :short)
" %-5s"
" %-8s"))
(defun prot-modeline--evil-propertize-tag (state variant &optional face)
"Propertize STATE tag of VARIANT with optional FACE.
VARIANT of the state tag is either :short or :long, as defined in
`prot-modeline-evil-state-tags'. If FACE is nil, fall back to
`default'."
(propertize
(format (prot-modeline--evil-get-format-specifier variant) (prot-modeline--evil-get-tag state variant))
'face (or face 'mode-line)
'mouse-face 'mode-line-highlight
'help-echo (format "Evil `%s' state" state)))
(defun prot-modeline-evil-state-tag (variant)
"Return mode line tag VARIANT depending on the Evil state.
VARIANT of the state tag is either :short or :long, as defined in
`prot-modeline-evil-state-tags'."
(pcase evil-state
('normal (prot-modeline--evil-propertize-tag 'normal variant 'prot-modeline-indicator-blue))
('insert (prot-modeline--evil-propertize-tag 'insert variant)) ; I don't actually use an "insert" state: it switches to "emacs"
('visual (pcase evil-visual-selection
('line (prot-modeline--evil-propertize-tag 'vline variant 'prot-modeline-indicator-yellow))
('screen-line (prot-modeline--evil-propertize-tag 'vsline variant 'prot-modeline-indicator-yellow))
('block (prot-modeline--evil-propertize-tag 'vblock variant 'prot-modeline-indicator-yellow))
(_ (prot-modeline--evil-propertize-tag 'visual variant 'prot-modeline-indicator-yellow))))
('motion (prot-modeline--evil-propertize-tag 'motion variant 'prot-modeline-indicator-yellow))
('emacs (prot-modeline--evil-propertize-tag 'emacs variant 'prot-modeline-indicator-magenta))
('operator (prot-modeline--evil-propertize-tag 'operator variant 'prot-modeline-indicator-red))
('replace (prot-modeline--evil-propertize-tag 'replace variant 'prot-modeline-indicator-red))
('prot-basic (prot-modeline--evil-propertize-tag 'prot-basic variant 'prot-modeline-indicator-green))))
(defvar-local prot-modeline-evil
'(:eval
(when (window-dedicated-p)
(propertize " = "
'face 'prot-modeline-indicator-gray-bg
'mouse-face 'mode-line-highlight)))
"Mode line construct for dedicated window indicator.")
(if (and (mode-line-window-selected-p) (bound-and-true-p evil-mode))
(let ((variant (if (prot-modeline--truncate-p) :short :long)))
(prot-modeline-evil-state-tag variant))
" "))
"Mode line construct to display the Evil state.")
;;;; Buffer name and modified status
@ -329,9 +355,16 @@ Specific to the current window's mode line.")
(defun prot-modeline--buffer-name ()
"Return `buffer-name', truncating it if necessary.
See `prot-modeline-string-cut-middle'."
(when-let* ((name (buffer-name)))
(prot-modeline-string-cut-middle name)))
See `prot-modeline-string-truncate'."
(when-let ((name (buffer-name)))
(prot-modeline-string-truncate name)))
(defun prot-modeline-buffer-name ()
"Return buffer name, with read-only indicator if relevant."
(let ((name (prot-modeline--buffer-name)))
(if buffer-read-only
(format "%s %s" (char-to-string #xE0A2) name)
name)))
(defun prot-modeline-buffer-name-help-echo ()
"Return `help-echo' value for `prot-modeline-buffer-identification'."
@ -344,27 +377,25 @@ See `prot-modeline-string-cut-middle'."
'face 'font-lock-doc-face)))
(defvar-local prot-modeline-buffer-identification
'(:eval
(let* ((name (propertize (prot-modeline--buffer-name)
'face (prot-modeline-buffer-identification-face)
'mouse-face 'mode-line-highlight
'help-echo (prot-modeline-buffer-name-help-echo)))
(read-only-icon (prot-icons-get-icon
'read-only
(unless (mode-line-window-selected-p)
'prot-modeline-indicator-gray))))
(if buffer-read-only
(format "%s %s" read-only-icon name)
name)))
'(:eval
(propertize (prot-modeline-buffer-name)
'face (prot-modeline-buffer-identification-face)
'mouse-face 'mode-line-highlight
'help-echo (prot-modeline-buffer-name-help-echo)))
"Mode line construct for identifying the buffer being displayed.
Propertize the current buffer with the `mode-line-buffer-id'
face. Let other buffers have no face.")
;;;; Major mode
(defun prot-modeline-major-mode-icon ()
"Return icon for the major mode."
(prot-icons-get-icon major-mode 'prot-modeline-indicator-gray))
(defun prot-modeline-major-mode-indicator ()
"Return appropriate propertized mode line indicator for the major mode."
(let ((indicator (cond
((derived-mode-p 'text-mode) "§")
((derived-mode-p 'prog-mode) "λ")
((derived-mode-p 'comint-mode) ">_")
(t ""))))
(propertize indicator 'face 'shadow)))
(defun prot-modeline-major-mode-name ()
"Return capitalized `major-mode' without the -mode suffix."
@ -372,7 +403,7 @@ face. Let other buffers have no face.")
(defun prot-modeline-major-mode-help-echo ()
"Return `help-echo' value for `prot-modeline-major-mode'."
(if-let* ((parent (get major-mode 'derived-mode-parent)))
(if-let ((parent (get major-mode 'derived-mode-parent)))
(format "Symbol: `%s'. Derived from: `%s'" major-mode parent)
(format "Symbol: `%s'." major-mode)))
@ -381,7 +412,7 @@ face. Let other buffers have no face.")
(propertize "%[" 'face 'prot-modeline-indicator-red)
'(:eval
(concat
(prot-modeline-major-mode-icon)
(prot-modeline-major-mode-indicator)
" "
(propertize
(prot-modeline-string-abbreviate-but-last
@ -402,11 +433,31 @@ face. Let other buffers have no face.")
(defun prot-modeline--vc-branch-name (file backend)
"Return capitalized VC branch name for FILE with BACKEND."
(when-let* ((rev (vc-working-revision file backend))
(branch (or (vc-git--symbolic-ref file)
(substring rev 0 7))))
(when-let ((rev (vc-working-revision file backend))
(branch (or (vc-git--symbolic-ref file)
(substring rev 0 7))))
(capitalize branch)))
;; NOTE 2023-07-27: This is a good idea, but it hardcodes Git, whereas
;; I want a generic VC method. Granted, I only use Git but I still
;; want it to work as a VC extension.
;; (defun prot-modeline-diffstat (file)
;; "Return shortened Git diff numstat for FILE."
;; (when-let* ((output (shell-command-to-string (format "git diff --numstat %s" file)))
;; (stats (split-string output "[\s\t]" :omit-nulls "[\s\f\t\n\r\v]+"))
;; (added (nth 0 stats))
;; (deleted (nth 1 stats)))
;; (cond
;; ((and (equal added "0") (equal deleted "0"))
;; "")
;; ((and (not (equal added "0")) (equal deleted "0"))
;; (propertize (format "+%s" added) 'face 'shadow))
;; ((and (equal added "0") (not (equal deleted "0")))
;; (propertize (format "-%s" deleted) 'face 'shadow))
;; (t
;; (propertize (format "+%s -%s" added deleted) 'face 'shadow)))))
(declare-function vc-git-working-revision "vc-git" (file))
(defvar prot-modeline-vc-map
@ -414,7 +465,7 @@ face. Let other buffers have no face.")
(define-key map [mode-line down-mouse-1] 'vc-diff)
(define-key map [mode-line down-mouse-3] 'vc-root-diff)
map)
"Keymap for the `prot-modeline-vc-branch'.")
"Keymap to display on VC indicator.")
(defun prot-modeline--vc-help-echo (file)
"Return `help-echo' message for FILE tracked by VC."
@ -424,20 +475,23 @@ face. Let other buffers have no face.")
(defun prot-modeline--vc-text (file branch &optional face)
"Prepare text for Git controlled FILE, given BRANCH.
With optional FACE, use it to propertize the BRANCH."
(format "%s %s "
(prot-icons-get-icon 'git 'prot-modeline-indicator-gray)
;; (propertize (char-to-string #xE0A0) 'face 'prot-modeline-indicator-gray)
(propertize branch
'face face
'mouse-face 'mode-line-highlight
'help-echo (prot-modeline--vc-help-echo file)
'local-map prot-modeline-vc-map)))
(concat
(propertize (char-to-string #xE0A0) 'face 'shadow)
" "
(propertize branch
'face face
'mouse-face 'mode-line-highlight
'help-echo (prot-modeline--vc-help-echo file)
'local-map prot-modeline-vc-map)
;; " "
;; (prot-modeline-diffstat file)
))
(defun prot-modeline--vc-details (file branch &optional face)
"Return Git BRANCH details for FILE, truncating it if necessary.
The string is truncated if the width of the window is smaller
than `split-width-threshold'."
(prot-modeline-string-cut-end
(prot-modeline-string-truncate
(prot-modeline--vc-text file branch face)))
(defvar prot-modeline--vc-faces
@ -452,18 +506,17 @@ than `split-width-threshold'."
(defun prot-modeline--vc-get-face (key)
"Get face from KEY in `prot-modeline--vc-faces'."
(alist-get key prot-modeline--vc-faces 'vc-up-to-date-state))
(alist-get key prot-modeline--vc-faces 'up-to-date))
(defun prot-modeline--vc-face (file backend)
"Return VC state face for FILE with BACKEND."
(when-let* ((key (vc-state file backend)))
(prot-modeline--vc-get-face key)))
(prot-modeline--vc-get-face (vc-state file backend)))
(defvar-local prot-modeline-vc-branch
'(:eval
(when-let* (((mode-line-window-selected-p))
(file (or buffer-file-name default-directory))
(backend (or (vc-backend file) 'Git))
(file (buffer-file-name))
(backend (vc-backend file))
;; ((vc-git-registered file))
(branch (prot-modeline--vc-branch-name file backend))
(face (prot-modeline--vc-face file backend)))
@ -497,18 +550,18 @@ TYPE is usually keyword `:error', `:warning' or `:note'."
(defmacro prot-modeline-flymake-type (type indicator &optional face)
"Return function that handles Flymake TYPE with stylistic INDICATOR and FACE."
`(defun ,(intern (format "prot-modeline-flymake-%s" type)) ()
(when-let* ((count (prot-modeline-flymake-counter
,(intern (format ":%s" type)))))
(when-let ((count (prot-modeline-flymake-counter
,(intern (format ":%s" type)))))
(concat
(propertize ,indicator 'face 'prot-modeline-indicator-gray)
(propertize ,indicator 'face 'shadow)
(propertize count
'face ',(or face type)
'mouse-face 'mode-line-highlight
;; FIXME 2023-07-03: Clicking on the text with
;; this buffer and a single warning present, the
;; diagnostics take up the entire frame. Why?
'local-map prot-modeline-flymake-map
'help-echo "mouse-1: buffer diagnostics\nmouse-3: project diagnostics")))))
'mouse-face 'mode-line-highlight
;; FIXME 2023-07-03: Clicking on the text with
;; this buffer and a single warning present, the
;; diagnostics take up the entire frame. Why?
'local-map prot-modeline-flymake-map
'help-echo "mouse-1: buffer diagnostics\nmouse-3: project diagnostics")))))
(prot-modeline-flymake-type error "")
(prot-modeline-flymake-type warning "!")
@ -539,52 +592,6 @@ Specific to the current window's mode line.")
"Mode line construct displaying Eglot information.
Specific to the current window's mode line.")
;;;; Frame name
(defcustom prot-modeline-show-frame-name nil
"When non-nil, display the current frame name."
:type 'boolean)
(defvar prot-modeline-frame-name-map
(let ((map (make-sparse-keymap)))
(define-key map [mode-line down-mouse-1] 'buffer-menu)
map)
"Keymap for the `prot-modeline-frame-name'.")
(defvar-local prot-modeline-frame-name
'(prot-modeline-show-frame-name
(" "
(:eval
(when-let* ((_ (mode-line-window-selected-p))
(current-frame (selected-frame))
(_ (frame-live-p current-frame))
(parameters (frame-parameters))
(name (capitalize (alist-get 'name parameters))))
(format "%s %s "
(prot-icons-get-icon 'frame 'prot-modeline-indicator-gray)
(propertize name
'mouse-face 'mode-line-highlight
'help-echo (format "Current frame name\nmouse-1: `buffer-menu'")
'local-map prot-modeline-frame-name-map))))))
"Mode line construct to display the current frame name.")
;;;; `which-function-mode' indicator
(defvar-local prot-modeline-which-function-indicator
'(which-function-mode
(:eval
(when (or (provided-mode-derived-p major-mode 'text-mode)
(provided-mode-derived-p major-mode 'prog-mode))
(let ((string (or (gethash (selected-window) which-func-table) which-func-unknown)))
(propertize string
'face '(prot-modeline-indicator-small which-func)
'mouse-face 'mode-line-highlight
'help-echo (format "Current definition: `%s'" string))))))
"The equivalent of `which-func-format'.")
(with-eval-after-load 'which-func
(setq mode-line-misc-info (delete (assq 'which-function-mode mode-line-misc-info) mode-line-misc-info)))
;;;; Miscellaneous
(defvar-local prot-modeline-notmuch-indicator
@ -610,15 +617,14 @@ Specific to the current window's mode line.")
prot-modeline-narrow
prot-modeline-input-method
prot-modeline-buffer-status
prot-modeline-window-dedicated-status
prot-modeline-evil
prot-modeline-buffer-identification
prot-modeline-major-mode
prot-modeline-process
prot-modeline-vc-branch
prot-modeline-flymake
prot-modeline-eglot
prot-modeline-frame-name
prot-modeline-which-function-indicator
;; prot-modeline-align-right
prot-modeline-notmuch-indicator
prot-modeline-misc-info))
(put construct 'risky-local-variable t))

View file

@ -1,8 +1,8 @@
;;; prot-notmuch.el --- Tweaks for my notmuch.el configurations -*- lexical-binding: t -*-
;; Copyright (C) 2021-2026 Protesilaos
;; Copyright (C) 2021-2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -26,6 +26,11 @@
;;
;; This covers my tweaks for notmuch.el that are meant for use in my
;; Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:

View file

@ -1,8 +1,8 @@
;;; prot-orderless.el --- Extensions for Orderless -*- lexical-binding: t -*-
;; Copyright (C) 2020-2026 Protesilaos
;; Copyright (C) 2020-2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -52,8 +52,8 @@
(defun prot-orderless-beg-or-end (word _index _total)
"Expand WORD~ to \\(^WORD\\|WORD$\\)."
(when-let* (((string-suffix-p "~" word))
(word (substring word 0 -1)))
(when-let (((string-suffix-p "~" word))
(word (substring word 0 -1)))
`(orderless-regexp . ,(format "\\(^%s\\|%s$\\)" word word))))
(provide 'prot-orderless)

View file

@ -1,8 +1,8 @@
;;; prot-org.el --- Tweaks for my org-mode configurations -*- lexical-binding: t -*-
;; Copyright (C) 2021-2026 Protesilaos
;; Copyright (C) 2021-2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -26,6 +26,11 @@
;;
;; This covers my tweaks for Org that are meant for use in my
;; Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
@ -38,6 +43,83 @@
;;;; org-capture
(defvar prot-org--capture-coach-person-history nil)
(declare-function message-fetch-field "message" (header &optional first))
(declare-function notmuch-show-get-header "notmuch-show")
(defun prot-org--capture-coach-person-message-from ()
"Return default value for `prot-org--capture-coach-person-prompt'."
(when-let ((from (cond
((derived-mode-p 'message-mode)
(message-fetch-field "To"))
((derived-mode-p 'notmuch-show-mode)
(notmuch-show-get-header :From)))))
(string-clean-whitespace (car (split-string from "<")))))
(defun prot-org--capture-coach-person-message-from-and-subject ()
"Return default value for `prot-org--capture-coach-person-prompt'."
(cond
((derived-mode-p 'message-mode)
(message-fetch-field "Subject"))
((derived-mode-p 'notmuch-show-mode)
(notmuch-show-get-header :Subject))))
(defun prot-org--capture-coach-person-prompt ()
"Prompt for person for use in `prot-org-capture-coach'."
(completing-read "Person to coach: "
prot-org--capture-coach-person-history
nil nil nil
'prot-org--capture-coach-person-history
(prot-org--capture-coach-person-message-from)))
(defvar prot-org--capture-coach-description-history nil)
(defun prot-org--capture-coach-description-prompt ()
"Prompt for description in `prot-org-capture-coach'."
(read-string "Description: "
nil
'prot-org--capture-coach-description-history
(prot-org--capture-coach-person-message-from-and-subject)))
(defun prot-org--capture-coach-date-prompt-range ()
"Prompt for Org date and return it as a +1h range.
For use in `prot-org-capture-coach'."
(let ((date (org-read-date :with-time)))
;; We cannot use this here, unfortunately, as the Org agenda
;; interprets it both as a deadline and an event with the date
;; range.
;;
;; (format "DEADLINE: <%s>--<%s>\n" date
(format "<%s>--<%s>\n" date
(org-read-date
:with-time nil "++1h" nil
(org-encode-time (org-parse-time-string date))))))
(defun prot-org-capture-coach ()
"Contents of an Org capture template for my coaching lessons."
(concat "* COACH " (prot-org--capture-coach-person-prompt) " "
(prot-org--capture-coach-description-prompt) " :lesson:\n"
;; See comment above
;; (prot-org--capture-coach-date-prompt-range)
"DEADLINE: %^T\n"
":PROPERTIES:\n"
":CAPTURED: %U\n"
":APPT_WARNTIME: 20\n"
":END:\n\n"
"%a%?"))
(defun prot-org-capture-coach-clock ()
"Contents of an Org capture for my clocked coaching services."
(concat "* COACH " (prot-org--capture-coach-person-prompt) " "
(prot-org--capture-coach-description-prompt) " :service:\n"
;; See comment above
;; (prot-org--capture-coach-date-prompt-range)
":PROPERTIES:\n"
":CAPTURED: %U\n"
":END:\n\n"
"%a%?"))
(declare-function cl-letf "cl-lib")
;; Adapted from source: <https://stackoverflow.com/a/54251825>.
@ -47,7 +129,6 @@
;; we were missing the `&rest':
;; <https://github.com/org-roam/org-roam/issues/2142#issuecomment-1100718373>.
(defun prot-org--capture-no-delete-windows (&rest args)
"Apply ARGS while ignoring `delete-other-windows'."
(cl-letf (((symbol-function 'delete-other-windows) 'ignore))
(apply args)))
@ -55,228 +136,6 @@
(advice-add 'org-capture-place-template :around 'prot-org--capture-no-delete-windows)
(advice-add 'org-add-log-note :around 'prot-org--capture-no-delete-windows)
;;;;; Custom function to select a project to add to
(defun prot-org--get-outline (&optional file)
"Return `outline-regexp' headings and line numbers of current file or FILE."
(with-current-buffer (find-file-noselect file)
(let ((outline-regexp (format "^\\(?:%s\\)" (or (bound-and-true-p outline-regexp) "[*\^L]+")))
candidates)
(save-excursion
(goto-char (point-min))
(while (if (bound-and-true-p outline-search-function)
(funcall outline-search-function)
(re-search-forward outline-regexp nil t))
(push
;; NOTE 2024-11-24: The -5 (minimum width) is a sufficiently high number to keep the
;; alignment consistent in most cases. Larger files will simply shift the heading text
;; in minibuffer, but this is not an issue anymore.
(format "%-5s\t%s"
(line-number-at-pos (point))
(buffer-substring-no-properties (line-beginning-position) (line-end-position)))
candidates)
(goto-char (1+ (line-end-position)))))
(if candidates
(nreverse candidates)
(user-error "No outline")))))
(defvar prot-org-outline-history nil
"Minibuffer history for `prot-org-outline-prompt'.")
(defun prot-org-outline-prompt (&optional file)
"Prompt for outline among headings retrieved by `prot-org--get-outline'.
With optional FILE use the outline of it, otherwise use that of
the current file."
(let ((current-file (or file buffer-file-name))
(default (car prot-org-outline-history)))
(completing-read
(format-prompt
(format "Select heading inside `%s': "
(propertize (file-name-nondirectory current-file) 'face 'error))
default)
(prot-common-completion-table-no-sort 'imenu (prot-org--get-outline current-file))
nil :require-match nil 'prot-org-outline-history default)))
(defvar prot-org-file-history nil
"Minibuffer history of `prot-org-file-prompt'.")
(defun prot-org--not-useful-p (file)
"Return non-nil if FILE is not a useful Org file for `org-capture'."
(or (string-match-p "\\.org_archive\\'" file)
(backup-file-name-p file)
(not (string-match-p "\\.org\\'" file))))
(defun prot-org-file-prompt ()
"Select a file in the `org-directory'."
(if-let* ((dir org-directory)
(files (directory-files-recursively org-directory ".*" nil))
(files (seq-remove #'prot-org--not-useful-p files)))
(let ((default (car prot-org-file-history)))
(completing-read
(format-prompt "Select file" default)
(prot-common-completion-table 'file files)
nil :require-match nil 'prot-org-file-history default))
(user-error "There are no files in the `org-directory'")))
;;;###autoload
(defun prot-org-select-heading-in-file ()
"Move to heading in Org file stored in `org-directory'.
Prompt for file, then for heading inside of that file.
This function can also be used as part of an `org-capture' template to
navigate to a file+heading and then capture something which is inserted
there."
(declare (interactive-only t))
(interactive)
(pcase-let* ((file (prot-org-file-prompt))
(line-with-heading (prot-org-outline-prompt file))
(`(,line ,text) (split-string line-with-heading "\t"))
(line (string-to-number line)))
;; NOTE 2024-11-24: `with-current-buffer' does not work with `org-capture'.
(find-file file)
(goto-char (point-min))
(forward-line (1- line))))
(defalias 'prot-org-goto-heading-in-file 'prot-org-select-heading-in-file
"Alias for `prot-org-select-heading-in-file'.")
;;;; Org clock report
(defvar prot-org-clock--template-with-effort
"#+BEGIN: clocktable :formula % :properties (\"Effort\") :timestamp t :sort (1 . ?a) :link t :scope nil :hidefiles t :maxlevel 8 :stepskip0 t
#+END:"
"Clock table with effort estimate column to use for custom clock reports.")
(defvar prot-org-clock--template-no-effort
"#+BEGIN: clocktable :formula % :timestamp t :sort (1 . ?a) :link nil :scope nil :hidefiles t :maxlevel 8 :stepskip0 t
#+END:"
"Clock table to use for custom clock reports.")
(defvar prot-org-clock--ranges
'( today yesterday thisweek lastweek thismonth
lastmonth thisyear lastyear untilnow)
"Time ranges of my interest for clock reports.")
(defvar prot-org-clock--report-range-history nil
"Minibuffer history for `prot-org-clock--report-range-prompt'.")
(defun prot-org-clock--report-range-prompt ()
"Prompt for a clock table range among `prot-org-clock--ranges'."
(let ((default (car prot-org-clock--report-range-history)))
(completing-read
(format-prompt "Select a time range for the clock" default)
prot-org-clock--ranges nil :require-match nil 'prot-org-clock--report-range-history
default)))
(defun prot-org-clock--get-report (scope)
"Produce clock report with current file SCOPE and return its buffer.
SCOPE is a symbol of either `file' or `subtree'. If the former, then
use the entire file's contents. Else use those of the current subtree."
(let ((buffer (get-buffer-create "*prot-org-custom-clock-report*")))
(save-restriction
(unwind-protect
(progn
(pcase scope
('file nil)
('subtree (org-narrow-to-subtree))
(_ (error "The scope `%s' is unknown" scope)))
(let ((contents (buffer-substring (point-min) (point-max))))
(with-current-buffer buffer
(erase-buffer)
(org-mode)
(save-excursion
(insert (format "%s\n\n" prot-org-clock--template-with-effort))
(insert contents))
(save-excursion
(let ((range (prot-org-clock--report-range-prompt)))
(goto-char (line-end-position))
(insert (concat " :block " range))))
(org-dblock-update))))
(widen)))
buffer))
;;;###autoload
(defun prot-org-clock-report-current-subtree-or-file (&optional whole-buffer)
"Produce a clock report in a new buffer for the subtree at point.
With optional WHOLE-BUFFER as a non-nil value, operate on the entire file.
When called interactively WHOLE-BUFFER is a prefix argument."
(interactive "P")
(when-let* ((buffer (prot-org-clock--get-report (if whole-buffer 'file 'subtree))))
(pop-to-buffer buffer)))
;;;###autoload
(defun prot-org-clock-select-heading-and-clock-report ()
"Select a heading in a file and do a clock report for it in a new buffer."
(interactive)
(call-interactively 'prot-org-select-heading-in-file)
(call-interactively 'prot-org-clock-report-current-subtree-or-file))
;;;;; Coaching-related Org custom clocking
;; TODO 2024-12-15: This sort of thing must exist in Org, but I did
;; not find it.
(defun prot-org--timestamp-to-time (string)
"Return time object of STRING timestamp."
(org-timestamp-to-time (org-timestamp-from-string string)))
(defun prot-org-coach--get-entries (todo-keyword string since)
"Get Org entries matching TODO-KEYWORD followed by STRING in the heading.
Limit entries to those whole deadline/scheduled is equal or greater to
SINCE date.
Each entry is a plist of :heading, :contents, :started, :closed."
(or (delq nil
(org-map-entries
(lambda ()
(when-let* ((case-fold-search t)
(started (prot-org--timestamp-to-time (or (org-entry-get nil "DEADLINE") (org-entry-get nil "SCHEDULED"))))
(closed (prot-org--timestamp-to-time (org-entry-get nil "CLOSED")))
((re-search-forward (format "\\<%s\\>.*\\<%s\\>" todo-keyword string) (line-end-position) t 1))
((org-time-less-p since started)))
(list
:heading (org-get-heading :no-tags :no-todo :no-priority :no-comment)
:contents (org-get-entry)
:started started
:closed closed)))))
(user-error "No entries with heading matching `\\<%s\\>.*\\<%s\\>'" todo-keyword string)))
(defvar prot-org-coach--name-history nil
"Minibuffer history of `prot-org-coach--name-prompt'.")
(defun prot-org-coach--name-prompt ()
"Prompt for name of person."
(let ((default (car prot-org-coach--name-history)))
(read-string
(format-prompt "Name of person" default)
nil 'prot-org-coach--name-history default)))
;;;###autoload
(defun prot-org-coach-report (name since)
"Produce clock report for coaching with person of NAME.
SINCE is the date (of time 00:00) to count from until now."
(interactive
(list
(prot-org-coach--name-prompt)
(format "[%s]" (org-read-date))))
(if-let* ((since-object (prot-org--timestamp-to-time since))
(entries (prot-org-coach--get-entries "done" name since-object))
(buffer (get-buffer-create "*prot-org-coach-entries*")))
(with-current-buffer (pop-to-buffer buffer)
(erase-buffer)
(org-mode)
(dolist (entry entries)
(insert (format "* %s\n%s\n\n" (plist-get entry :heading) (plist-get entry :contents)))
(org-clock-in nil (plist-get entry :started))
(org-clock-out nil t (plist-get entry :closed)))
(goto-char (point-min))
(save-excursion
(insert (format "%s\n\n" prot-org-clock--template-no-effort)))
(save-excursion
(goto-char (line-end-position))
(insert (format " :tstart %S" since)))
(org-dblock-update))
(user-error "No entries for name `%s'" name)))
;;;; org-agenda
(declare-function calendar-day-name "calendar")
@ -316,17 +175,6 @@ produces dates with a fixed length."
(defvar org-priority-highest)
(defun prot-org-agenda-include-priority-no-timestamp ()
"Return nil if heading has a priority but no timestamp.
Otherwise, return the buffer position from where the search should
continue, per `org-agenda-skip-function'."
(let ((point (point)))
(if (and (eq (nth 3 (org-heading-components)) ?A)
(not (org-get-deadline-time point))
(not (org-get-scheduled-time point)))
nil
(line-beginning-position 2))))
(defvar prot-org-custom-daily-agenda
;; NOTE 2021-12-08: Specifying a match like the following does not
;; work.
@ -336,19 +184,13 @@ continue, per `org-agenda-skip-function'."
;; So we match everything and then skip entries with
;; `org-agenda-skip-function'.
`((tags-todo "*"
((org-agenda-overriding-header "Important tasks without a date\n")
;; NOTE 2024-10-31: Those used to work, but now the
;; query for the timestamp is ignored. I thus wrote
;; `prot-org-agenda-include-priority-no-timestamp'.
;;
;; (org-agenda-skip-function '(org-agenda-skip-subtree-if nil '(timestamp)))
;; (org-agenda-skip-function
;; `(org-agenda-skip-entry-if
;; 'notregexp ,(format "\\[#%s\\]" (char-to-string org-priority-highest))))
(org-agenda-skip-function #'prot-org-agenda-include-priority-no-timestamp)
(org-agenda-block-separator nil)))
(agenda "" ((org-agenda-overriding-header "\nPending scheduled tasks")
(org-agenda-time-grid nil)
((org-agenda-skip-function '(org-agenda-skip-if nil '(timestamp)))
(org-agenda-skip-function
`(org-agenda-skip-entry-if
'notregexp ,(format "\\[#%s\\]" (char-to-string org-priority-highest))))
(org-agenda-block-separator nil)
(org-agenda-overriding-header "Important tasks without a date\n")))
(agenda "" ((org-agenda-time-grid nil)
(org-agenda-start-on-weekday nil)
(org-agenda-span 1)
(org-agenda-show-all-dates nil)
@ -358,43 +200,30 @@ continue, per `org-agenda-skip-function'."
(org-agenda-block-separator nil)
(org-agenda-entry-types '(:scheduled))
(org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
(org-agenda-skip-function '(org-agenda-skip-entry-if 'regexp "ROUTINE"))
(org-agenda-skip-function '(org-agenda-skip-entry-if 'regexp "TRAIN"))
(org-agenda-category-filter "-habit")
(org-agenda-day-face-function (lambda (date) 'org-agenda-date))
(org-agenda-format-date "")))
(agenda "" ((org-agenda-overriding-header "\nToday's agenda\n")
(org-agenda-span 1)
(org-agenda-format-date "")
(org-agenda-overriding-header "\nPending scheduled tasks")))
(agenda "" ((org-agenda-span 1)
(org-deadline-warning-days 0)
(org-agenda-block-separator nil)
(org-scheduled-past-days 0)
(org-agenda-skip-function '(org-agenda-skip-entry-if 'regexp "ROUTINE"))
;; We don't need the `org-agenda-date-today'
;; highlight because that only has a practical
;; utility in multi-day views.
(org-agenda-day-face-function (lambda (date) 'org-agenda-date))
(org-agenda-format-date "%A %-e %B %Y")))
;; (agenda "" ((org-agenda-overriding-header "\nRoutine")
;; (org-agenda-time-grid nil)
;; (org-agenda-start-on-weekday nil)
;; (org-agenda-span 1)
;; (org-agenda-show-all-dates nil)
;; (org-scheduled-past-days 365)
;; ;; Excludes today's scheduled items
;; (org-scheduled-delay-days 1)
;; (org-agenda-block-separator nil)
;; (org-agenda-entry-types '(:scheduled))
;; (org-agenda-skip-function '(org-agenda-skip-entry-if 'notregexp "ROUTINE"))
;; (org-agenda-day-face-function (lambda (date) 'org-agenda-date))
;; (org-agenda-format-date "")))
(agenda "" ((org-agenda-overriding-header "\nNext three days\n")
(org-agenda-start-on-weekday nil)
(org-agenda-format-date "%A %-e %B %Y")
(org-agenda-overriding-header "\nToday's agenda\n")))
(agenda "" ((org-agenda-start-on-weekday nil)
(org-agenda-start-day nil)
(org-agenda-start-day "+1d")
(org-agenda-span 3)
(org-deadline-warning-days 0)
(org-agenda-block-separator nil)
(org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))))
(agenda "" ((org-agenda-overriding-header "\nUpcoming deadlines (+14d)\n")
(org-agenda-time-grid nil)
(org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
(org-agenda-overriding-header "\nNext three days\n")))
(agenda "" ((org-agenda-time-grid nil)
(org-agenda-start-on-weekday nil)
;; We don't want to replicate the previous section's
;; three days, so we start counting from the day after.
@ -404,32 +233,10 @@ continue, per `org-agenda-skip-function'."
(org-deadline-warning-days 0)
(org-agenda-block-separator nil)
(org-agenda-entry-types '(:deadline))
(org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done)))))
(org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
(org-agenda-overriding-header "\nUpcoming deadlines (+14d)\n"))))
"Custom agenda for use in `org-agenda-custom-commands'.")
(defun prot-org-custom-agenda-prot-asks-get-date ()
"Return the timestamp of the current heading or nil.
For use in `prot-org-custom-prot-asks-agenda'."
(when-let* ((timestamp (or (org-entry-get nil "DEADLINE") (org-entry-get nil "SCHEDULED")))
(time (prot-org--timestamp-to-time timestamp)))
(format-time-string "%10A %e %10B %6R" time)))
(defvar prot-org-custom-prot-asks-agenda
'((tags-todo "protasks"
((org-agenda-overriding-header "Prot Asks\n")
(org-agenda-prefix-format '((tags . "%(prot-org-custom-agenda-prot-asks-get-date) ")))
(org-agenda-sorting-strategy '(deadline-up))
(org-agenda-remove-tags t)
(org-agenda-block-separator nil))))
"Custom agenda for use in `org-agenda-custom-commands'.")
(defun prot-org-agenda-set-outline ()
"Set `outline-regexp' for my Org agenda buffers."
(when (derived-mode-p 'org-agenda-mode)
(setq-local outline-regexp "^\\([\s\t]+\\)\\([A-Z]+ \\|[0-9]+:[0-9]+ \\)\\(Deadline\\|Scheduled\\)")))
(add-hook 'org-agenda-mode-hook #'prot-org-agenda-set-outline)
;;;;; agenda appointments
(defvar prot-org-agenda-after-edit-hook nil
@ -497,37 +304,26 @@ To be used with `advice-add'.")
(declare-function org-id-new "org")
(declare-function org-entry-put "org")
;; Original idea:
;; Copied from this article (with minor tweaks from my side):
;; <https://writequit.org/articles/emacs-org-mode-generate-ids.html>.
(defun prot-org--id-get ()
"Get the CUSTOM_ID of the current entry.
If the entry already has a CUSTOM_ID, return it as-is, else
create a new one."
(let* ((pos (point))
(id (org-entry-get pos "CUSTOM_ID")))
(or (and id (stringp id) (string-match-p "\\S-" id))
(and (setq id (org-id-new "h")) (org-entry-put pos "CUSTOM_ID" id)))
id))
(defun prot-org--id-get (&optional pom create prefix)
"Get the CUSTOM_ID property of the entry at point-or-marker POM.
(defun prot-org--heading-to-id ()
"Convert current heading text to an ID for CUSTOM_ID purposes."
(thread-last
(org-get-heading :no-tags :no-todo :no-priority :no-comment)
(replace-regexp-in-string "[][{}!@#$%^&*()+'\"?,.\|;:~`‘’“”/=]*" "")
(replace-regexp-in-string "\s" "-")
(string-trim)
(downcase)
(concat "h:")))
(defun prot-org--id-get-readable (&optional force)
"Like `prot-org--id-get' but use the heading wording to create and ID.
With optional FORCE, update the value even if one exists."
(let* ((pos (point))
(id (unless force (org-entry-get pos "CUSTOM_ID"))))
(or (and id (stringp id) (string-match-p "\\S-" id))
(and (setq id (prot-org--heading-to-id))
(org-entry-put pos "CUSTOM_ID" id)))
id))
If POM is nil, refer to the entry at point. If the entry does
not have an CUSTOM_ID, the function returns nil. However, when
CREATE is non nil, create a CUSTOM_ID if none is present already.
PREFIX will be passed through to `org-id-new'. In any case, the
CUSTOM_ID of the entry is returned."
(org-with-point-at pom
(let ((id (org-entry-get nil "CUSTOM_ID")))
(cond
((and id (stringp id) (string-match "\\S-" id))
id)
(create
(setq id (org-id-new (concat prefix "h")))
(org-entry-put pom "CUSTOM_ID" id)
(org-id-add-location id (format "%s" (buffer-file-name (buffer-base-buffer))))
id)))))
(declare-function org-map-entries "org")
@ -535,93 +331,14 @@ With optional FORCE, update the value even if one exists."
(defun prot-org-id-headlines ()
"Add missing CUSTOM_ID to all headlines in current file."
(interactive)
(org-map-entries (lambda () (prot-org--id-get))))
(org-map-entries
(lambda () (prot-org--id-get (point) t))))
;;;###autoload
(defun prot-org-id-headlines-readable (&optional force)
"Like `prot-org-id-headlines' but with readable IDs.
A readable identifier is one derived from the text of the heading. In
theory, this may not be unique.
With optional FORCE, update the value even if one exists."
(interactive "P")
(org-map-entries (lambda () (prot-org--id-get-readable force))))
;;;###autoload
(defun prot-org-id-headline (&optional readable)
"Add missing CUSTOM_ID to headline at point.
With optional prefix argument READABLE get a readable identifier derived
from the heading text instead of a UUID."
(interactive "P")
(funcall (if readable 'prot-org--id-get-readable 'prot-org--id-get)))
;;;###autoload
(defun prot-org-get-dotemacs-link ()
"Get URL to current heading in my dotemacs file."
(declare (interactive-only t))
(defun prot-org-id-headline ()
"Add missing CUSTOM_ID to headline at point."
(interactive)
(if (and (derived-mode-p 'org-mode)
(string-match-p "prot-emacs\\.org\\'" buffer-file-name))
(if-let* ((id (org-entry-get (point) "CUSTOM_ID"))
(url (concat "https://protesilaos.com/emacs/dotemacs#" id)))
(progn
(kill-new url)
(message "Copied %s" (propertize url 'face 'success)))
(error "No CUSTOM_ID for the current entry"))
(user-error "You are not in the right file")))
;;;; Org and Diary integration
(defun prot-org-diary--get-file (ics-file)
"Return diary file path that reuses the name of ICS-FILE.
The path is relative to the `diary-file'."
(unless (file-exists-p diary-file)
(error "The `diary-file' does not exist"))
(let* ((directory (file-name-directory diary-file))
(ics-name (file-name-sans-extension (file-name-nondirectory ics-file)))
(diary-name (format "%s%s-%s-diary.txt" directory (format-time-string "%Y-%m-%d") ics-name)))
(when-let* ((buffer (get-file-buffer diary-name)))
(with-current-buffer buffer
(erase-buffer)))
(when (file-exists-p diary-name)
(delete-file diary-name))
diary-name))
(defun prot-org-diary-include-file (diary-file-to-include)
"Include DIARY-FILE-TO-INCLUDE in the `diary-file'."
(unless (file-exists-p diary-file)
(error "The `diary-file' does not exist"))
(with-current-buffer (find-file-noselect diary-file)
(let ((include-directive (format "#include %S" diary-file-to-include)))
(save-excursion
(goto-char (point-min))
(while (re-search-forward (format "^%s.*" include-directive) nil t)
(delete-region (match-beginning 0) (match-end 0)))
(goto-char (point-max))
(insert "\n")
(insert include-directive)
(insert "\n")
(save-buffer)))))
;;;###autoload
(defun prot-org-diary-import-ics (ics-file diary-file-to-include)
"Import ICS-FILE to DIARY-FILE."
(interactive
(let ((ics-file (read-file-name "Which ICS file? ")))
(list
ics-file
(prot-org-diary--get-file ics-file))))
(save-window-excursion
(icalendar-import-file ics-file diary-file-to-include))
(let ((diary-file-propertized (propertize diary-file-to-include 'face 'success))
(main-diary-file-propertized (propertize diary-file 'face 'warning)))
(when (yes-or-no-p (format
"Imported `%s' as `%s'\nINCLUDE IT in `%s'?"
(propertize ics-file 'face 'error)
diary-file-propertized
main-diary-file-propertized))
(prot-org-diary-include-file diary-file-to-include)
(message "Appended `%s' to `%s'" diary-file-propertized main-diary-file-propertized))))
(prot-org--id-get (point) t))
(provide 'prot-org)
;;; prot-org.el ends here

View file

@ -1,8 +1,8 @@
;;; prot-pair.el --- Insert character pair around symbol or region -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Protesilaos
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -26,6 +26,11 @@
;;
;; Insert character pair around symbol or region using minibuffer
;; completion.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
@ -34,8 +39,8 @@
:group 'editing)
(defcustom prot-pair-pairs
'((?' :description "Single quotes" :pair ?')
(?\" :description "Double quotes" :pair ?\")
'((?' :description "Single quotes" :pair (?' . ?'))
(?\" :description "Double quotes" :pair (?\" . ?\"))
(? :description "Single curly quotes" :pair (? . ?))
(?“ :description "Double curly quotes" :pair (?“ . ?”))
(?\> :description "Natural language quotes" :pair prot-pair-insert-natural-language-quotes)
@ -43,20 +48,20 @@
(?{ :description "Curly brackets" :pair (?{ . ?}))
(?\[ :description "Square brackets" :pair (?\[ . ?\]))
(?\< :description "Angled brackets" :pair (?\< . ?\>))
(?@ :description "At signs" :pair ?@)
(?= :description "Equals signs" :pair ?=)
(?+ :description "Plus signs" :pair ?+)
(?@ :description "At signs" :pair (?@ . ?@))
(?= :description "Equals signs" :pair (?= . ?=))
(?+ :description "Plus signs" :pair (?+ . ?+))
(?` :description "Backticks" :pair prot-pair-insert-backticks)
(?~ :description "Tildes" :pair ?~)
(?* :description "Asterisks" :pair ?*)
(?/ :description "Forward slashes" :pair ?/)
(?_ :description "Underscores" :pair ?_))
(?~ :description "Tildes" :pair (?~ . ?~))
(?* :description "Asterisks" :pair (?* . ?*))
(?/ :description "Forward slashes" :pair (?/ . ?/))
(?_ :description "Underscores" :pair (?_ . ?_)))
"Alist of pairs for use with `prot-pair-insert'.
Each element in the list is a list whose `car' is a character and
the `cdr' is a plist with a `:description' and `:pair' keys. The
`:description' is a string used to describe the character/pair in
interactive use, while `:pair' is either a cons cell referencing the
opening and closing characters or a single character.
interactive use, while `:pair' is a cons cell referencing the
opening and closing characters.
The value of `:pair' can also be the unquoted symbol of a
function. The function is called with no arguments and must
@ -93,29 +98,27 @@ backtick and single quote, else two backticks."
(defun prot-pair--annotate (character)
"Annotate CHARACTER with its description in `prot-pair-pairs'."
(when-let* ((char (if (characterp character) character (string-to-char character)))
(plist (alist-get char prot-pair-pairs))
(description (plist-get plist :description)))
(when-let ((char (if (characterp character) character (string-to-char character)))
(plist (alist-get char prot-pair-pairs))
(description (plist-get plist :description)))
(format " %s" description)))
(defun prot-pair--get-pair (character)
"Get the pair corresponding to CHARACTER."
(when-let* ((plist (alist-get character prot-pair-pairs))
(pair-value (plist-get plist :pair)))
(if (characterp pair-value)
(cons pair-value pair-value)
pair-value)))
"Get the pair of corresponding to CHARACTER."
(when-let ((char (if (characterp character) character (string-to-char character)))
(plist (alist-get char prot-pair-pairs))
(pair (plist-get plist :pair)))
pair))
(defun prot-pair--insert-prompt ()
"Prompt for pair among `prot-pair-pairs'."
(let* ((default (car prot-pair--insert-history))
(candidates (mapcar (lambda (char) (char-to-string (car char))) prot-pair-pairs))
(completion-extra-properties `(:annotation-function ,#'prot-pair--annotate))
(choice (completing-read
(format-prompt "Select pair" default)
candidates nil :require-match
nil 'prot-pair--insert-history default)))
(string-to-char choice)))
(let ((default (car prot-pair--insert-history))
(candidates (mapcar (lambda (char) (char-to-string (car char))) prot-pair-pairs))
(completion-extra-properties `(:annotation-function ,#'prot-pair--annotate)))
(completing-read
(format-prompt "Select pair" default)
candidates nil :require-match
nil 'prot-pair--insert-history default)))
(defun prot-pair--insert-bounds ()
"Return boundaries of symbol at point or active region."
@ -123,44 +126,29 @@ backtick and single quote, else two backticks."
(cons (region-beginning) (region-end))
(bounds-of-thing-at-point 'symbol)))
(defun prot-pair-insert-subr (character n)
"Insert pair of CHARACTER as many as N times."
(if-let* ((pair (prot-pair--get-pair character)))
(let* ((bounds (prot-pair--insert-bounds))
(beg (car bounds))
(end (1+ (cdr bounds))) ; 1+ because we want the character after it
(characters (if (functionp pair) (funcall pair) pair)))
(dotimes (_ n)
(save-excursion
(goto-char beg)
(insert (car characters))
(goto-char end)
(setq end (1+ end))
(insert (cdr characters))))
(goto-char (+ end (1- n))))
(user-error "Cannot find character `%s' in `prot-pair-pairs'" (char-to-string character))))
;;;###autoload
(defun prot-pair-insert (character n)
"Insert CHARACTER pair an N number of times around object at point.
CHARACTER is one among `prot-pair-pairs'. The object at point is either
a symbol or the boundaries of the active region. N is a numeric prefix
argument, defaulting to 1 if none is provided in interactive use."
(defun prot-pair-insert (pair n)
"Insert N number of PAIR around object at point.
PAIR is one among `prot-pair-pairs'. The object at point is
either a symbol or the boundaries of the active region. N is a
numeric prefix argument, defaulting to 1 if none is provided in
interactive use."
(interactive
(list
(prot-pair--insert-prompt)
(prot-pair--get-pair (prot-pair--insert-prompt))
(prefix-numeric-value current-prefix-arg)))
(prot-pair-insert-subr character n))
;;;###autoload
(defun prot-pair-insert-directly (character n)
"Read CHARACTER, find it in `prot-pair-pairs', and insert it outright.
Do not use minibuffer completion like `prot-pair-insert'."
(interactive
(list
(read-char)
(prefix-numeric-value current-prefix-arg)))
(prot-pair-insert-subr character n))
(let* ((bounds (prot-pair--insert-bounds))
(beg (car bounds))
(end (1+ (cdr bounds))) ; 1+ because we want the character after it
(characters (if (functionp pair) (funcall pair) pair)))
(dotimes (_ n)
(save-excursion
(goto-char beg)
(insert (car characters))
(goto-char end)
(setq end (1+ end))
(insert (cdr characters))))
(goto-char (+ end (1- n)))))
;;;###autoload
(defun prot-pair-delete ()

View file

@ -1,8 +1,8 @@
;;; prot-prefix.el --- Prefix keymap for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Protesilaos
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -25,6 +25,11 @@
;;; Commentary:
;;
;; Prefix keymap for my custom keymaps.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
@ -37,7 +42,6 @@
(defvar-keymap prot-prefix-buffer-map
:doc "Prefix keymap for buffers."
:name "Buffer"
:prefix 'prot-prefix-buffer
"m" #'beframe-buffer-menu
"b" #'switch-to-buffer
"B" #'prot-simple-buffers-major-mode
@ -53,7 +57,6 @@
(defvar-keymap prot-prefix-file-map
:doc "Prefix keymaps for files."
:name "File"
:prefix 'prot-prefix-file
"f" #'find-file
"F" #'find-file-other-window
"b" #'bookmark-jump
@ -64,7 +67,6 @@
(defvar-keymap prot-prefix-insert-map
:doc "Prefix keymap for character insertion."
:name "Insert"
:prefix 'prot-prefix-insert
"i" #'insert-char
"e" #'emoji-search
"q" #'quoted-insert
@ -72,17 +74,16 @@
"l" #'emoji-list)
(declare-function logos-focus-mode "logos")
(declare-function keycast-mode-line-mode "keycast")
(declare-function keycast-mode "keycast")
(declare-function rainbow-mode "rainbow")
(declare-function spacious-padding-mode "spacious-padding")
(defvar-keymap prot-prefix-mode-map
:doc "Prefix keymap for minor mode toggles."
:name "Toggle"
:prefix 'prot-prefix-mode
"f" #'flymake-mode
"h" #'hl-line-mode
"k" #'keycast-mode-line-mode
"k" #'keycast-mode
"l" #'logos-focus-mode
"m" #'menu-bar-mode
"n" #'display-line-numbers-mode
@ -94,11 +95,9 @@
(defvar-keymap prot-prefix-window-map
:doc "Prefix keymap for windows."
:name "Window"
:prefix 'prot-prefix-window
"u" #'winner-undo
"r" #'winner-redo
"b" #'balance-windows-area
"d" #'toggle-window-dedicated
"0" #'delete-window
"1" #'delete-other-windows
"!" #'delete-other-windows-vertically
@ -128,7 +127,6 @@
(defvar-keymap prot-prefix-search-map
:doc "Prefix keymap for search (and replace) commands."
:name "Search"
:prefix 'prot-prefix-search
"f" #'consult-find
"d" #'prot-simple-flush-and-diff
"g" #'prot-search-grep
@ -147,7 +145,6 @@
(defvar-keymap prot-prefix-transpose-map
:doc "Prefix keymap for object transposition."
:name "Transpose"
:prefix 'prot-prefix-transpose
"c" #'prot-simple-transpose-chars
"l" #'prot-simple-transpose-lines
"p" #'prot-simple-transpose-paragraphs
@ -158,7 +155,6 @@
(defvar-keymap prot-prefix-expression-map
:doc "Prefix keymap for s-expression motions."
:name "S-EXP"
:prefix 'prot-prefix-expression
"a" #'beginning-of-defun
"e" #'end-of-defun
"f" #'forward-sexp
@ -176,21 +172,12 @@
(declare-function magit-status "magit" (&optional directory cache))
(declare-function prot-simple-other-windor-or-frame "prot-simple")
;; NOTE 2024-02-17: Some cons cells here have a symbol as a `cdr' and
;; some do not. The former are those which define a prefix command
;; (per `define-prefix-command'). This is a symbol that references
;; the keymaps, thus making our binding an indirection: if we update
;; the keymap, we automatically get the new key bindings. Whereas
;; when we bind a key to the value of a variable, we have to update
;; the keymap and then the binding for changes to propagate.
(defvar-keymap prot-prefix-map
:doc "Prefix keymap with multiple subkeymaps."
:name "Prot Prefix"
:prefix 'prot-prefix
"0" #'delete-window
"1" #'delete-other-windows
"!" #'delete-other-windows-vertically
"^" #'tear-off-window
"2" #'split-window-below
"@" #'split-root-window-below
"3" #'split-window-right
@ -198,43 +185,39 @@
"o" #'other-window
"O" #'prot-simple-other-windor-or-frame
"Q" #'save-buffers-kill-emacs
"b" (cons "Buffer" 'prot-prefix-buffer)
"b" prot-prefix-buffer-map
"c" #'world-clock
"f" (cons "File" 'prot-prefix-file)
"f" prot-prefix-file-map
"g" #'magit-status
"h" (cons "Help" help-map)
"i" (cons "Insert" 'prot-prefix-insert)
"h" help-map
"i" prot-prefix-insert-map
"j" #'dired-jump
"m" (cons "Minor modes" 'prot-prefix-mode)
"n" (cons "Narrow" narrow-map)
"p" (cons "Project" project-prefix-map)
"r" (cons "Rect/Registers" ctl-x-r-map)
"s" (cons "Search" 'prot-prefix-search)
"t" (cons "Transpose" 'prot-prefix-transpose)
"m" prot-prefix-mode-map
"n" narrow-map
"p" project-prefix-map
"r" ctl-x-r-map
"s" prot-prefix-search-map
"t" prot-prefix-transpose-map
"u" #'universal-argument
"v" (cons "Version Control" 'vc-prefix-map)
"w" (cons "Window" 'prot-prefix-window)
"x" (cons "S-EXP" 'prot-prefix-expression))
"v" vc-prefix-map
"w" prot-prefix-window-map
"x" prot-prefix-expression-map)
;; ;; NOTE 2024-02-17: This is not needed anymore, because I bind a cons
;; ;; cell to the key. The `car' of it is the description, which
;; ;; `which-key-mode' understands.
;;
;; (with-eval-after-load 'which-key
;; (which-key-add-keymap-based-replacements prot-prefix-map
;; "b" `("Buffer" . ,prot-prefix-buffer-map)
;; "f" `("File" . ,prot-prefix-file-map)
;; "h" `("Help" . ,help-map)
;; "i" `("Insert" . ,prot-prefix-insert-map)
;; "m" `("Mode" . ,prot-prefix-mode-map)
;; "n" `("Narrow" . ,narrow-map)
;; "p" `("Project" . ,project-prefix-map)
;; "r" `("C-x r" . ,ctl-x-r-map)
;; "s" `("Search" . ,prot-prefix-search-map)
;; "t" `("Transpose" . ,prot-prefix-transpose-map)
;; "v" `("C-x v" . ,vc-prefix-map)
;; "w" `("Window" . ,prot-prefix-window-map)
;; "x" `("S-EXP" . ,prot-prefix-expression-map)))
(with-eval-after-load 'which-key
(which-key-add-keymap-based-replacements prot-prefix-map
"b" `("Buffer" . ,prot-prefix-buffer-map)
"f" `("File" . ,prot-prefix-file-map)
"h" `("Help" . ,help-map)
"i" `("Insert" . ,prot-prefix-insert-map)
"m" `("Mode" . ,prot-prefix-mode-map)
"n" `("Narrow" . ,narrow-map)
"p" `("Project" . ,project-prefix-map)
"r" `("C-x r" . ,ctl-x-r-map)
"s" `("Search" . ,prot-prefix-search-map)
"t" `("Transpose" . ,prot-prefix-transpose-map)
"v" `("C-x v" . ,vc-prefix-map)
"w" `("Window" . ,prot-prefix-window-map)
"x" `("S-EXP" . ,prot-prefix-expression-map)))
;; What follows is an older experiment with transient. I like its
;; visuals, though find it hard to extend. Keymaps are easier for me,

View file

@ -1,176 +0,0 @@
;;; prot-project.el --- Extensions for project.el -*- lexical-binding: t -*-
;; Copyright (C) 2025-2026 Protesilaos
;; Author: Protesilaos <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or (at
;; your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Extensions for project.el.
;;; Code:
(require 'project)
(require 'tab-bar)
;;;; Switch to a project root Dired outright
(defun prot-project--switch (directory &optional command)
"Do the work of `project-switch-project' in the given DIRECTORY.
With optional COMMAND, run it in DIRECTORY."
(let ((command (or (when (functionp command) command)
(if (symbolp project-switch-commands)
project-switch-commands
(project--switch-project-command))))
(buffer (current-buffer)))
(unwind-protect
(progn
(setq-local project-current-directory-override directory)
(call-interactively command))
(with-current-buffer buffer
(kill-local-variable 'project-current-directory-override)))))
(defun prot-project--frame-names ()
"Return a list of frame names."
(mapcar #'car (make-frame-names-alist)))
(defvar prot-project-switch-hook nil
"Normal hook called after `prot-project-switch'.")
;;;###autoload
(defun prot-project-switch (directory)
"Switch to project DIRECTORY.
If DIRECTORY exists in a frame, select it. Otherwise switch to
the project in DIRECTORY using `project-dired'."
(interactive (list (funcall project-prompter)))
(project--remember-dir directory)
(let ((name (file-name-nondirectory (directory-file-name directory))))
(if (member name (prot-project--frame-names))
(select-frame-by-name name)
(prot-project--switch directory 'project-dired))
(run-hooks 'prot-project-switch-hook)
(setq this-command 'project-switch-project)))
;;;; Produce a VC root log for the project
(defun prot-project-rename-vc-root-log (&rest _)
"Rename the buffer of `vc-print-root-log' to mention the project."
(when-let* ((root (vc-root-dir))
((consp project--list))
((member root (mapcar #'car project--list))))
(rename-buffer (format "*vc-root-log: %s*" root))))
(advice-add #'vc-print-root-log :after #'prot-project-rename-vc-root-log)
;;;; One tab per project
;; NOTE 2024-01-15 07:07:52 +0200: I define the "in tab" functions as
;; a coding exercise. I don't have a use for it, as I prefer to use
;; the approach of my `beframe' package instead.
(defun prot-project-in-tab--get-tab-names (&optional frame)
"Return list of tab names associated with FRAME.
If FRAME is nil, use the current frame."
(mapcar
(lambda (tab)
(alist-get 'name tab))
(frame-parameter frame 'tabs)))
(defun prot-project-in-tab--create-tab (directory name)
"Create new tab visiting DIRECTORY and named NAME."
(tab-new)
(find-file directory)
(unwind-protect
(prot-project--switch directory 'project-dired)
(tab-rename name)
;; NOTE 2024-01-15 06:52 +0200: I am adding this because
;; `tab-rename' is not persistent for some reason. Probably a bug...
(let* ((tabs (funcall tab-bar-tabs-function))
(tab-to-rename (nth (tab-bar--current-tab-index) tabs)))
(setf (alist-get 'explicit-name tab-to-rename) name))))
;;;###autoload
(defun prot-project-in-tab (directory)
"Switch to project DIRECTORY in a tab.
If a tab is named after the non-directory component of DIRECTORY,
switch to it. Otherwise, create a new tab and name it after the
non-directory component of DIRECTORY.
Use this as an alternative to `project-switch-project'."
(interactive (list (funcall project-prompter)))
(project--remember-dir directory)
(let ((name (file-name-nondirectory (directory-file-name directory))))
(if (member name (prot-project-in-tab--get-tab-names))
(tab-switch name)
(prot-project-in-tab--create-tab directory name))
(setq this-command 'project-switch-project)))
;;;###autoload
(defun prot-project-maybe-in-tab ()
"Switch to project depending on `beframe-mode'.
When the mode is enabled (the expected behaviour), use the command
`prot-project-switch'. Otherwise, do `prot-project-in-tab'."
(interactive)
(call-interactively
(if (bound-and-true-p beframe-mode)
'prot-project-switch
'prot-project-in-tab)))
;;;; Set up a project root
;; I don't actually have a use-case for `prot-project-find-root',
;; but I wrote it once so I keep it here in case I ever need it.
;; Use it like this: (prot-project-find-root c-mode "Makefile")
(defmacro prot-project-find-root (mode file)
"Define project root check for MODE given FILE.
MODE must be the symbol of the major mode, without a quote. FILE
is a string."
(let ((project-find-fn (intern (format "project-find-%s-root" mode)))
(major-mode-fn (intern (format "prot-%s-project-find-function" mode)))
(file-symbol (intern file)))
`(progn
(defun ,project-find-fn (dir)
(when-let* ((root (locate-dominating-file dir ,file)))
(cons ',file-symbol root)))
(cl-defmethod project-root ((project (head ,file-symbol)))
(cdr project))
(defun ,(intern (format "prot-%s-project-find-function" mode)) ()
(add-hook 'project-find-functions #',project-find-fn :depth :local))
(add-hook ',(intern (format "%s-hook" mode)) #',major-mode-fn))))
;;;; Enable completion in the `compile' command
(defun prot-project-compilation-read-command (command)
"Like `compilation-read-command' for COMMAND with completion against the `compile-history'."
(if-let* ((history compile-history)
(default (car history)))
(completing-read "Compile command: " history nil nil nil 'compile-history default)
(read-shell-command "Compile command: " command
(if (equal (car compile-history) command)
'(compile-history . 1)
'compile-history))))
(advice-add #'compilation-read-command :override #'prot-project-compilation-read-command)
(provide 'prot-project)
;;; prot-project.el ends here

View file

@ -1,155 +0,0 @@
;;; prot-register.el --- Extensions for project.el -*- lexical-binding: t -*-
;; Copyright (C) 2025-2026 Protesilaos
;; Author: Protesilaos <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or (at
;; your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Extensions for register.el.
;;; Code:
(require 'register)
;;;; Define new register for file with point
(cl-defmethod register--type ((_regval vector)) 'vector)
(cl-defmethod register-val-describe ((val vector) _verbose)
(if-let* ((position (aref val 2))
(file (aref val 1)))
(princ (format "%s at position %s" file position))
(princ "Garbage data")))
;;;###autoload
(defun prot-simple-file-to-register (register)
"Store current location of file's point in REGISTER."
(interactive (list (register-read-with-preview "File with point to register: ")))
(set-register register (vector 'file-with-point (buffer-file-name) (point))))
(defvar prot-simple-file-to-register-jump-hook nil
"Normal hook called after jumping to a file register.
See `prot-simple-file-to-register'.")
;;;###autoload
(cl-defmethod register-val-jump-to ((val vector) delete)
"Handle how to jump to a location register.
This is like the default, but does not ask to visit a file: it does it
outright."
(cond
((eq (aref val 0) 'file-with-point)
(find-file (aref val 1))
(goto-char (aref val 2))
(run-hooks 'prot-simple-file-to-register-jump-hook))
(t (cl-call-next-method val delete))))
;;;; Do-What-I-Mean commands
;;;###autoload
(defun prot-register-add-dwim (register &optional number)
"Do-What-I-Mean with REGISTER.
If the region is active, call `copy-to-register'.
If the optional prefix argument NUMBER is non-nil, then call
`number-to-register'.
If there are more than two windows or there are more than one
`tab-bar-mode' tabs, then do `frameset-to-register'.
Otherwise, use `prot-simple-file-to-register'.
Also see `prot-register-use-dwim'."
(interactive
(list
(register-read-with-preview "Add register: ")
(when current-prefix-arg
(prefix-numeric-value current-prefix-arg))))
(cond
((when-let* ((_ (region-active-p))
(beg (region-beginning))
(end (region-end))
(text (buffer-substring beg end))
(_ (not (string-blank-p text))))
(copy-to-register register beg end)
(message "Copied `%s' to register `%c'" text register)))
(number
(number-to-register number register)
(message "Copied number `%d' to register `%c'" number register))
((or (length> (window-list) 2)
(and (bound-and-true-p tab-bar-mode)
(length> (tab-bar-tabs) 1)))
(frameset-to-register register)
(message "Copied current frameset to register `%c'" register))
(t
(prot-simple-file-to-register register)
(message "Copied current point in file to register `%c'" register))))
;;;###autoload
(defun prot-register-use-dwim (register)
"Use the REGISTER, jumping or inserting it, depending on its ocntents."
(interactive (list (register-read-with-preview "Use register: ")))
(let ((contents (get-register register)))
(cond
((register--jumpable-p contents)
(jump-to-register register))
((or (register--insertable-p contents)
(numberp register))
(insert-register register))
(t
(error "The register is unknown: %S" contents)))))
;;;; The `prot-register-persist-mode'
(defcustom prot-register-save-file (locate-user-emacs-file "prot-register.eld")
"File to store registers to and retrieve from."
:type 'file)
(defun prot-register-store ()
"Write `register-alist' to `prot-register-save-file'."
(with-temp-file prot-register-save-file
(insert ";; Auto-generated file: DO NOT EDIT -*- mode: emacs-lisp -*-\n")
(pp register-alist (current-buffer))))
(defun prot-register-load ()
"Read `prot-register-save-file' and return its contents."
(with-temp-buffer
(when (file-exists-p prot-register-save-file)
(insert-file-contents prot-register-save-file)
(read (buffer-string)))))
(defun prot-register-watcher (symbol newval operation where)
(when (and (eq operation 'set) (null where) newval)
(prot-register-store)))
;;;###autoload
(define-minor-mode prot-register-persist-mode
"When enabled save `register-alist' to `prot-register-save-file' when a change occurs."
:globat t
(if prot-register-persist-mode
(progn
(add-variable-watcher 'register-alist #'prot-register-watcher)
(add-hook 'kill-emacs-hook #'prot-register-store))
(remove-variable-watcher 'register-alist #'prot-register-watcher)
(remove-hook 'kill-emacs-hook #'prot-register-store)))
(provide 'prot-register)
;;; prot-register.el ends here

View file

@ -1,8 +1,8 @@
;;; prot-scratch.el --- Scratch buffers for editable major mode of choice -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Protesilaos
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -27,6 +27,11 @@
;; Set up a scratch buffer for an editable major mode of choice. The
;; idea is based on the `scratch.el' package by Ian Eure:
;; <https://github.com/ieure/scratch-el>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:

View file

@ -1,8 +1,8 @@
;;; prot-search.el --- Extensions to isearch, replace, grep for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2020-2026 Protesilaos
;; Copyright (C) 2020-2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -26,6 +26,11 @@
;;
;; This covers my isearch.el, replace.el, and grep.el extensions, for
;; use in my Emacs setup: <https://protesilaos.com/emacs/dotemacs>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
@ -89,6 +94,51 @@ search started."
(isearch-pop-state)))
(isearch-update))
;;;###autoload
(defun prot-search-isearch-repeat-forward (&optional arg)
"Move forward, keeping point at the beginning of the match.
Optionally move to ARGth match in the given direction."
(interactive "p")
(when (and isearch-forward isearch-other-end)
(goto-char isearch-other-end))
(isearch-repeat-forward (or arg 1)))
;;;###autoload
(defun prot-search-isearch-repeat-backward (&optional arg)
"Move backward, keeping point at the beginning of the match.
Optionally move to ARGth match in the given direction."
(interactive "p")
(when (and (not isearch-forward) isearch-other-end)
(goto-char isearch-other-end))
(isearch-repeat-backward (or arg 1)))
(defmacro prot-search-isearch-occurrence (name edge &optional doc)
"Construct function for moving to `isearch' occurrence.
NAME is the name of the function. EDGE is either the beginning
or the end of the buffer. Optional DOC is the resulting
function's docstring."
`(defun ,name (&optional arg)
,doc
(interactive "p")
(let ((x (or arg 1))
(command (intern (format "isearch-%s-of-buffer" ,edge))))
(isearch-forward-symbol-at-point)
(funcall command x))))
(prot-search-isearch-occurrence
prot-search-isearch-beginning-of-buffer
"beginning"
"Run `isearch-beginning-of-buffer' for the symbol at point.
With numeric ARG, move to ARGth occurrence counting from the
beginning of the buffer.")
(prot-search-isearch-occurrence
prot-search-isearch-end-of-buffer
"end"
"Run `isearch-end-of-buffer' for the symbol at point.
With numeric ARG, move to ARGth occurrence counting from the
end of the buffer.")
;;;; Replace/Occur
(defvar prot-search-markup-replacements
@ -137,7 +187,7 @@ boundaries, else start from point to the end of the buffer."
(user-error "`%s' is not part of `prot-search-markup-replacements'" type)))
;; NOTE 2023-01-14: See my `substitute' package instead of the
;; following: <https://github.com/protesilaos/substitute>.
;; following: <https://git.sr.ht/~protesilaos/substitute>.
;; (defun prot-search-isearch-replace-symbol ()
;; "Run `query-replace-regexp' for the symbol at point."
@ -145,31 +195,6 @@ boundaries, else start from point to the end of the buffer."
;; (isearch-forward-symbol-at-point)
;; (isearch-query-replace-regexp))
(defvar prot-search-collect-history nil
"Minibuffer history for `prot-search-collect'.")
;;;###autoload
(defun prot-search-collect (regexp)
"Return matches for REGEXP as a substring in the current buffer.
Put the results in a *prot-search-collect* buffer."
(interactive
(list
(format "\\<\\w*%s\\w*\\>" (read-string "Collect REGEXP: " nil 'prot-search-collect-history))))
(save-excursion
(goto-char (point-min))
(let ((matches nil))
(while (re-search-forward regexp nil t)
(push (match-string 0) matches))
(when matches
(setq matches (nreverse matches))
(let ((buffer (get-buffer-create "*prot-search-collect*")))
(with-current-buffer buffer
(erase-buffer)
(dolist (match matches)
(insert match)
(insert "\n")))
(display-buffer buffer))))))
(autoload 'goto-address-mode "goto-addr")
;;;###autoload
@ -268,52 +293,6 @@ Also see `prot-search-grep-todo-keywords'."
(buf-name (format "*keywords in <%s>*" (buffer-name))))
(occur-1 prot-search-todo-keywords num (list (current-buffer)) buf-name)))
;;;; Outline
(defun prot-search--get-outline ()
"Return alist of outline outline-regexp and positions."
(let* ((outline-regexp (format "^\\(?:%s\\)" (or (bound-and-true-p outline-regexp) "[*\^L]+")))
(heading-alist (bound-and-true-p outline-heading-alist))
(level-fun (or (bound-and-true-p outline-level)
(lambda () ;; as in the default from outline.el
(or (cdr (assoc (match-string 0) heading-alist))
(- (match-end 0) (match-beginning 0))))))
candidates)
(save-excursion
(goto-char (point-min))
(while (if (bound-and-true-p outline-search-function)
(funcall outline-search-function)
(re-search-forward outline-regexp nil t))
(push
(format "%-5s %s"
(line-number-at-pos (point))
(buffer-substring-no-properties (line-beginning-position) (line-end-position)))
candidates)
(goto-char (1+ (line-end-position)))))
(if candidates
(nreverse candidates)
(user-error "No outline"))))
(defun prot-search--outline-prompt ()
"Prompt for outline among headings retrieved by `prot-search--get-outline'."
(completing-read
"Go to outline: "
(prot-common-completion-table-no-sort 'imenu (prot-search--get-outline))
nil :require-match))
(defvar prot-search-outline-hook nil
"Normal hook to run at the end of `prot-search-outline'.")
;;;###autoload
(defun prot-search-outline ()
"Go to the line of the given outline using completion."
(interactive)
(when-let* ((selection (prot-search--outline-prompt))
(line (string-to-number (car (split-string selection "\t")))))
(goto-char (point-min))
(forward-line (- line 1))
(run-hooks 'prot-search-outline-hook)))
;;;; Grep
(defvar prot-search--grep-hist nil
@ -504,22 +483,18 @@ and function as links to the context they reference."
prot-search--find-file-names-args
"dired")
;; TODO 2026-08-15: I want to revise all of the above functions that
;; involve find/grep. Is `process-lines' the better option?
;;;###autoload
(defun prot-search-grep-to-dired (regexp)
"Produce a Dired buffer with files matching REGEXP.
Recursively search for files from the current directory using the find
program."
(interactive
(list
(read-regexp "Find and grep for REGEXP: " nil 'prot-search--find-grep-hist)))
(let ((files (process-lines "find"
"-type" "f"
"-exec" "grep" "-nHE" "--color=auto" regexp
"-ls" "{}" ";")))
(dired (cons default-directory files))))
;; (defun prot-search-find-grep-file (regexp)
;; "Use find to produce list of files that include REGEXP."
;; (interactive
;; (list
;; (read-regexp "Find and grep for REGEXP: " nil 'prot-search--find-grep-hist)))
;; (let ((files (process-lines "find"
;; "-type" "f"
;; "-exec" "grep" "-nHE" "--color=auto" (format "'%s" regexp) " "
;; "-ls" " "
;; "{};")
;; ))
;; (find-file (completing-read "Find file: "files))))
(provide 'prot-search)
;;; prot-search.el ends here

View file

@ -1,8 +1,8 @@
;;; prot-shell.el --- M-x shell extensions for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Protesilaos
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -26,6 +26,11 @@
;;
;; This covers my shell.el extensions, for use in my Emacs setup:
;; <https://protesilaos.com/emacs/dotemacs>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
@ -104,24 +109,18 @@ Only account for the history Emacs knows about, ignoring
(defun prot-shell-track-cd (&rest _)
"Track shell input of cd commands.
Push `shell-last-dir' to `prot-shell-cd-directories'."
(when-let* ((input (prot-shell--last-input))
((string-match-p "cd " input)))
(when-let ((input (prot-shell--last-input))
((string-match-p "cd " input)))
(push shell-last-dir prot-shell-cd-directories)))
(defun prot-shell-update-name-on-cd (&rest _)
"Update the shell buffer name after a cd for use in `prot-shell'."
(when-let* ((input (prot-shell--last-input))
((string-match-p "cd " input)))
(rename-buffer (format "*prot-shell in %s*" default-directory) :make-unique)))
(defvar prot-shell--cd-history nil
"Minibuffer history for `prot-shell-cd'.")
(defun prot-shell--cd-prompt ()
"Prompt for a directory among `prot-shell-cd-directories'."
(if-let* ((history prot-shell-cd-directories)
(dirs (cons default-directory history))
(def (if (listp dirs) (car dirs) shell-last-dir)))
(if-let ((history prot-shell-cd-directories)
(dirs (cons default-directory history))
(def (if (listp dirs) (car dirs) shell-last-dir)))
(completing-read
(format-prompt "Select directory" def)
dirs nil :require-match nil 'prot-shell--cd-history def)
@ -147,104 +146,101 @@ Push `shell-last-dir' to `prot-shell-cd-directories'."
(defun prot-shell-cd-vc-root-dir ()
"Change into the `vc-root-dir'."
(interactive)
(if-let* ((root (prot-shell--get-vc-root-dir)))
(if-let ((root (prot-shell--get-vc-root-dir)))
(prot-shell--insert-and-send "cd" root)
(user-error "Cannot find the VC root of `%s'" default-directory)))
;; NOTE 2025-06-23: Emacs 31 supports shell bookmarks, so I no longer
;; need this. I am keeping it here for reference.
;;;; Bookmark support
;; ;;;; Bookmark support
;;
;; ;; NOTE 2023-08-18: I sent this to the Emacs maintainers as a patch
;; ;; (bug#65039). I received approval to proceed with the change, but I
;; ;; did not do it because a user reported an issue with SSH (TRAMP). I
;; ;; do not have access to SSH and am not familiar with such workflows.
;; ;; If/when that changes, I will try again. In the meantime, this is
;; ;; good code and it works for me.
;;
;; ;; Adapted from esh-mode.el
;; (declare-function bookmark-prop-get "bookmark" (bookmark prop))
;;
;; (defun prot-shell-bookmark-name ()
;; "Return name of bookmark based on currect directory."
;; (format "prot-shell-%s"
;; (file-name-nondirectory
;; (directory-file-name
;; (file-name-directory default-directory)))))
;;
;; (defvar sh-shell-file)
;;
;; (defun prot-shell-bookmark-make-record ()
;; "Create a bookmark for the current Shell buffer."
;; `(,(prot-shell-bookmark-name)
;; (location . ,default-directory)
;; (shell-file-name . ,sh-shell-file)
;; (handler . prot-shell-bookmark-jump)))
;;
;; ;;;###autoload
;; (defun prot-shell-bookmark-jump (bookmark)
;; "Default BOOKMARK handler for Shell buffers."
;; (let ((default-directory (bookmark-prop-get bookmark 'location))
;; (explicit-shell-file-name (bookmark-prop-get bookmark 'shell-file-name)))
;; (shell (get-buffer-create (car bookmark)))))
;;
;; (put 'prot-shell-bookmark-jump 'bookmark-handler-type "Shell")
;; NOTE 2023-08-18: I sent this to the Emacs maintainers as a patch
;; (bug#65039). I received approval to proceed with the change, but I
;; did not do it because a user reported an issue with SSH (TRAMP). I
;; do not have access to SSH and am not familiar with such workflows.
;; If/when that changes, I will try again. In the meantime, this is
;; good code and it works for me.
;; ;;;; Convert YouTube links to Invidious
;;
;; (defvar prot-shell-invidious-domains
;; '("invidious.io.lol"
;; "invidious.lunar.icu"
;; "iv.nboeck.de"
;; "vid.priv.au"
;; "invidious.tiekoetter.com"
;; "inv.in.projectsegfau.lt"
;; "onion.tube"
;; "yt.artemislena.eu"
;; "invidious.no-logs.com"
;; "yewtu.be"
;; "invidious.projectsegfau.lt"
;; "yt.oelrichsgarcia.de"
;; "invidious.0011.lt"
;; "inv.zzls.xyz"
;; "inv.bp.projectsegfau.lt"
;; "invidious.flokinet.to"
;; "iv.ggtyler.dev"
;; "invidious.slipfox.xyz"
;; "vid.puffyan.us"
;; "inv.pistasjis.net"
;; "inv.citw.lgbt"
;; "invidious.protokolla.fi"
;; "inv.makerlab.tech"
;; "inv.tux.pizza"
;; "invidious.privacydev.net")
;; "List of Invidious domains.")
;;
;; (defvar prot-shell-youtube-domains
;; '("www.youtube.com"
;; "youtu.be")
;; "List of YouTube domains.")
;;
;; (defvar prot-shell-yt-invidious-domains
;; (append prot-shell-youtube-domains
;; prot-shell-invidious-domains)
;; "List of YouTube and Invidious domains.")
;;
;; (defun prot-shell--get-random-invidious-instance ()
;; "Return `random' index from `prot-shell-invidious-domains'."
;; (nth
;; (random (length prot-shell-invidious-domains))
;; prot-shell-invidious-domains))
;;
;; ;;;###autoload
;; (defun prot-shell-invidious ()
;; "Convert `prot-shell-yt-invidious-domains' into a random Invidious instance."
;; (interactive)
;; (save-excursion
;; (goto-char (line-beginning-position))
;; (while (re-search-forward (regexp-opt prot-shell-yt-invidious-domains) (line-end-position) :no-error)
;; (replace-match (prot-shell--get-random-invidious-instance)))))
;; Adapted from esh-mode.el
(declare-function bookmark-prop-get "bookmark" (bookmark prop))
(defun prot-shell-bookmark-name ()
"Return name of bookmark based on currect directory."
(format "prot-shell-%s"
(file-name-nondirectory
(directory-file-name
(file-name-directory default-directory)))))
(defvar sh-shell-file)
(defun prot-shell-bookmark-make-record ()
"Create a bookmark for the current Shell buffer."
`(,(prot-shell-bookmark-name)
(location . ,default-directory)
(shell-file-name . ,sh-shell-file)
(handler . prot-shell-bookmark-jump)))
;;;###autoload
(defun prot-shell-bookmark-jump (bookmark)
"Default BOOKMARK handler for Shell buffers."
(let ((default-directory (bookmark-prop-get bookmark 'location))
(explicit-shell-file-name (bookmark-prop-get bookmark 'shell-file-name)))
(shell (get-buffer-create (car bookmark)))))
(put 'prot-shell-bookmark-jump 'bookmark-handler-type "Shell")
;;;; Convert YouTube links to Invidious
(defvar prot-shell-invidious-domains
'("invidious.io.lol"
"invidious.lunar.icu"
"iv.nboeck.de"
"vid.priv.au"
"invidious.tiekoetter.com"
"inv.in.projectsegfau.lt"
"onion.tube"
"yt.artemislena.eu"
"invidious.no-logs.com"
"yewtu.be"
"invidious.projectsegfau.lt"
"yt.oelrichsgarcia.de"
"invidious.0011.lt"
"inv.zzls.xyz"
"inv.bp.projectsegfau.lt"
"invidious.flokinet.to"
"iv.ggtyler.dev"
"invidious.slipfox.xyz"
"vid.puffyan.us"
"inv.pistasjis.net"
"inv.citw.lgbt"
"invidious.protokolla.fi"
"inv.makerlab.tech"
"inv.tux.pizza"
"invidious.privacydev.net")
"List of Invidious domains.")
(defvar prot-shell-youtube-domains
'("www.youtube.com"
"youtu.be")
"List of YouTube domains.")
(defvar prot-shell-yt-invidious-domains
(append prot-shell-youtube-domains
prot-shell-invidious-domains)
"List of YouTube and Invidious domains.")
(defun prot-shell--get-random-invidious-instance ()
"Return `random' index from `prot-shell-invidious-domains'."
(nth
(random (length prot-shell-invidious-domains))
prot-shell-invidious-domains))
;;;###autoload
(defun prot-shell-invidious ()
"Convert `prot-shell-yt-invidious-domains' into a random Invidious instance."
(interactive)
(save-excursion
(goto-char (line-beginning-position))
(while (re-search-forward (regexp-opt prot-shell-yt-invidious-domains) (line-end-position) :no-error)
(replace-match (prot-shell--get-random-invidious-instance)))))
;;;; Built-in Emacs commands
@ -302,23 +298,14 @@ input, otherwise perform buffer motion."
(interactive "^p")
(prot-shell--history-or-motion 'comint-next-input 'next-line arg))
;;;###autoload
(defun prot-shell ()
"Like `shell' but always start a new shell.
Name the shell buffer after the `default-directory'. If the name of
that buffer already exists, then reuse it."
(interactive)
(with-current-buffer (shell (format "*prot-shell in %s*" default-directory))
(add-hook 'comint-output-filter-functions #'prot-shell-update-name-on-cd nil :local)))
;;;; Minor mode setup
(defvar-keymap prot-shell-mode-map
:doc "keymap for `prot-shell-mode'."
:doc "Key map for `prot-shell-mode'."
"<up>" #'prot-shell-up-dwim
"<down>" #'prot-shell-down-dwim
"C-c C-d" #'prot-shell-cd
;; "C-c C-i" #'prot-shell-invidious
"C-c C-i" #'prot-shell-invidious
"C-c C-j" #'prot-shell-input-from-history
"C-c C-." #'prot-shell-cd-vc-root-dir
"C-c C-r" #'prot-shell-cd-vc-root-dir)

View file

@ -1,8 +1,8 @@
;;; prot-simple.el --- Common commands for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2020-2026 Protesilaos
;; Copyright (C) 2020-2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -25,6 +25,11 @@
;;; Commentary:
;;
;; Common commands for my Emacs: <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
@ -52,39 +57,6 @@ Used by `prot-simple-inset-date'."
;;;; General commands
;;;###autoload
(defun prot-simple-describe-at-point (symbol)
"Describe the SYMBOL at point.
If there is no symbol or the symbol at point does not satisfy `symbolp',
prompt for one."
(interactive (list (intern-soft (thing-at-point 'symbol))))
(if symbol
(describe-symbol symbol)
(call-interactively 'describe-symbol)))
;;;###autoload
(defun prot-simple-indent-dwim ()
"Indent the current defun in `prog-mode' or paragraph in `text-mode'."
(interactive)
(save-excursion
(cond
((derived-mode-p 'prog-mode)
(mark-defun))
((derived-mode-p 'text-mode)
(mark-paragraph)))
(indent-for-tab-command)
(deactivate-mark)))
;;;###autoload
(defun prot-simple-sudo ()
"Find the current file or directory using `sudo'."
(interactive)
(let ((destination (or buffer-file-name default-directory))
(auto-save-default nil))
(if (string= (file-remote-p destination 'method) "sudo")
(user-error "Already using `sudo'")
(find-file (format "/sudo::/%s" destination)))))
(defun prot-simple--mark (bounds)
"Mark between BOUNDS as a cons cell of beginning and end positions."
(push-mark (car bounds))
@ -95,17 +67,14 @@ prompt for one."
(defun prot-simple-mark-sexp ()
"Mark symbolic expression at or near point.
Repeat to extend the region forward to the next symbolic
expression."
expression. If inside a string, mark the entire string upon
repeat."
(interactive)
(if (and (region-active-p)
(eq last-command this-command))
(ignore-errors (forward-sexp 1))
(when-let* ((thing (cond
((thing-at-point 'url) 'url)
((thing-at-point 'sexp) 'sexp)
((thing-at-point 'string) 'string)
((thing-at-point 'word) 'word))))
(prot-simple--mark (bounds-of-thing-at-point thing)))))
(if (eq last-command this-command)
(if-let ((bounds (bounds-of-thing-at-point 'string)))
(prot-simple--mark bounds)
(ignore-errors (forward-sexp 1)))
(prot-simple--mark (bounds-of-thing-at-point 'sexp))))
;;;###autoload
(defun prot-simple-keyboard-quit-dwim ()
@ -132,90 +101,69 @@ The DWIM behaviour of this command is as follows:
(t
(keyboard-quit))))
(autoload 'symbol-at-point "thingatpt")
;;;###autoload
(defun prot-simple-delete-window-dwim ()
"Do What I Mean to delete the current THING.
When there is more than one window, THING is a window.
When there are more than one `tab-bar-mode' tabs, THING is a tab.
Else THING is a frame if frames are more than one."
(declare (interactive-only t))
(defun prot-simple-describe-symbol ()
"Run `describe-symbol' for the `symbol-at-point'."
(interactive)
(cond
((length> (window-list) 1)
(delete-window))
((and (featurep 'tab-bar)
(length> (tab-bar-tabs) 1))
(tab-close))
((length> (frame-list) 1)
(delete-frame))
(t
(user-error "Nothing to delete"))))
(describe-symbol (symbol-at-point)))
;; DEPRECATED 2023-12-26: The `prot-simple-goto-definition' is a good
;; idea but it needs more work. Ultimately though, it is easier to
;; just produce a Help buffer and just go to the source from there by
;; typing 's'.
(declare-function help--symbol-completion-table "help-fns" (string pred action))
;; (declare-function help--symbol-completion-table "help-fns" (string pred action))
;;
;; ;;;###autoload
;; (defun prot-simple-goto-definition (symbol)
;; "Prompt for SYMBOL and go to its source.
;; When called from Lisp, SYMBOL is a string."
;; (interactive
;; (list
;; (completing-read "Go to source of SYMBOL: "
;; #'help--symbol-completion-table
;; nil :require-match)))
;; (xref-find-definitions symbol))
;;;###autoload
(defun prot-simple-goto-definition (symbol)
"Prompt for SYMBOL and go to its source.
When called from Lisp, SYMBOL is a string."
(interactive
(list
(completing-read "Go to source of SYMBOL: "
#'help--symbol-completion-table
nil :require-match)))
(xref-find-definitions symbol))
;; DEPRECATED 2023-12-26: I have no need for these commands. I was
;; just experimenting with a simple implementation. It is not robust.
;; I can fix it, but I will still not use it, so I am deprecating it
;; instead.
(autoload 'number-at-point "thingatpt")
;; (autoload 'number-at-point "thingatpt")
;;
;; (defun prot-simple--number-operate (number amount operation)
;; "Perform OPERATION on NUMBER given AMOUNT and return the result.
;; OPERATION is the keyword `:increment' or `:decrement' to perform
;; `1+' or `1-', respectively."
;; (when (and (numberp number) (numberp amount))
;; (let ((fn (pcase operation
;; (:increment #'+)
;; (:decrement #'-)
;; (_ (user-error "Unknown operation `%s' for number `%s'" operation number)))))
;; (funcall fn number amount))))
;;
;; (defun prot-simple--number-replace (number amount operation)
;; "Perform OPERATION on NUMBER at point by AMOUNT."
;; (when-let* ((bounds (bounds-of-thing-at-point 'number))
;; (replacement (prot-simple--number-operate number amount operation)))
;; (delete-region (car bounds) (cdr bounds))
;; (save-excursion
;; (insert (number-to-string replacement)))))
;;
;; ;;;###autoload
;; (defun prot-simple-number-increment (number amount)
;; "Increment NUMBER by AMOUNT.
;; When called interactively, NUMBER is the one at point, while
;; AMOUNT is either 1 or that of a number prefix argument."
;; (interactive
;; (list
;; (number-at-point)
;; (prefix-numeric-value current-prefix-arg)))
;; (prot-simple--number-replace number amount :increment))
;;
;; ;;;###autoload
;; (defun prot-simple-number-decrement (number amount)
;; "Decrement NUMBER by AMOUNT.
;; When called interactively, NUMBER is the one at point, while
;; AMOUNT is either 1 or that of a number prefix argument."
;; (interactive
;; (list
;; (number-at-point)
;; (prefix-numeric-value current-prefix-arg)))
;; (prot-simple--number-replace number amount :decrement))
(defun prot-simple--number-operate (number amount operation)
"Perform OPERATION on NUMBER given AMOUNT and return the result.
OPERATION is the keyword `:increment' or `:decrement' to perform
`1+' or `1-', respectively."
(when (and (numberp number) (numberp amount))
(let ((fn (pcase operation
(:increment #'+)
(:decrement #'-)
(_ (user-error "Unknown operation `%s' for number `%s'" operation number)))))
(funcall fn number amount))))
(defun prot-simple--number-replace (number amount operation)
"Perform OPERATION on NUMBER at point by AMOUNT."
(when-let ((bounds (bounds-of-thing-at-point 'number))
(replacement (prot-simple--number-operate number amount operation)))
(delete-region (car bounds) (cdr bounds))
(save-excursion
(insert (number-to-string replacement)))))
;;;###autoload
(defun prot-simple-number-increment (number amount)
"Increment NUMBER by AMOUNT.
When called interactively, NUMBER is the one at point, while
AMOUNT is either 1 or that of a number prefix argument."
(interactive
(list
(number-at-point)
(prefix-numeric-value current-prefix-arg)))
(prot-simple--number-replace number amount :increment))
;;;###autoload
(defun prot-simple-number-decrement (number amount)
"Decrement NUMBER by AMOUNT.
When called interactively, NUMBER is the one at point, while
AMOUNT is either 1 or that of a number prefix argument."
(interactive
(list
(number-at-point)
(prefix-numeric-value current-prefix-arg)))
(prot-simple--number-replace number amount :decrement))
;;;; Commands for lines
@ -253,61 +201,6 @@ When called interactively without a prefix numeric argument, N is
(make-obsolete 'prot-simple-copy-line-or-region 'prot-simple-copy-line "2023-09-26")
;;;###autoload
(defun prot-simple-kill-ring-save (&optional beg end)
"Copy the current region or line.
When the region is active, use `kill-ring-save' between the BEG and END
positions. Otherwise, copy the current line."
;; NOTE 2025-02-23: Using (interactive "r") returns an error before
;; running the body of this function if there is no mark. This
;; happens when visiting a file.
(interactive
(when (region-active-p)
(list
(region-beginning)
(region-end))))
(if (and beg end)
(copy-region-as-kill beg end)
(prot-simple-copy-line))
(setq this-command 'kill-ring-save))
;;;###autoload
(defun prot-simple-kill-region (&optional beg end)
"Do `kill-region' when the region is active, else `kill-ring-save' symbol at point."
(interactive
(when (region-active-p)
(list
(region-beginning)
(region-end))))
(if (and beg end)
(kill-region beg end)
(prot-simple-mark-sexp)
(copy-region-as-kill (region-beginning) (region-end)))
(setq this-command 'kill-ring-save))
(defun prot-simple--duplicate-buffer-substring (boundaries)
"Duplicate buffer substring between BOUNDARIES.
BOUNDARIES is a cons cell representing buffer positions."
(unless (consp boundaries)
(error "`%s' is not a cons cell" boundaries))
(let ((beg (car boundaries))
(end (cdr boundaries)))
(goto-char end)
(newline)
(insert (buffer-substring-no-properties beg end))))
;;;###autoload
(defun prot-simple-duplicate-line-or-region ()
"Duplicate the current line or active region."
(interactive)
(unless mark-ring ; needed when entering a new buffer
(push-mark (point) t nil))
(prot-simple--duplicate-buffer-substring
(if (region-active-p)
(cons (region-beginning) (region-end))
(cons (line-beginning-position) (line-end-position))))
(setq this-command 'yank))
;;;###autoload
(defun prot-simple-yank-replace-line-or-region ()
"Replace line or region with latest kill.
@ -317,8 +210,7 @@ This command can then be followed by the standard
(if (use-region-p)
(delete-region (region-beginning) (region-end))
(delete-region (line-beginning-position) (line-end-position)))
(yank)
(setq this-command 'yank))
(yank))
;;;###autoload
(defun prot-simple-multi-line-below ()
@ -338,75 +230,7 @@ This command can then be followed by the standard
(defun prot-simple-kill-line-backward ()
"Kill from point to the beginning of the line."
(interactive)
(kill-line 0)
(setq this-command 'kill-line))
;;;###autoload
(defun prot-simple-copy-line-forward (n)
"Copy from point to the end of the Nth line.
Without numeric prefix argument N, operate on the current line."
(interactive "p")
(let ((point (point))
(end (line-end-position n))
(max (point-max)))
(copy-region-as-kill
point
(if (> end max)
max
end)))
(setq this-command 'kill-ring-save))
;;;###autoload
(defun prot-simple-copy-line-backward (n)
"Copy from point to the beginning of the Nth line.
Without numeric prefix argument N, operate on the current line."
(interactive "p")
(let ((point (point))
(beg (line-beginning-position n))
(min (point-min)))
(copy-region-as-kill
point
(if (< beg min)
min
beg)))
(setq this-command 'kill-ring-save))
;;;###autoload
(defun prot-simple-delete-line ()
"Delete (not kill) from point to the end of the line."
(interactive)
(let* ((point (point))
(end (line-end-position))
(end+ (+ end 1)))
(cond
((> end+ (point-max)) (delete-region point end))
((= point end) (delete-region point end+))
(t (delete-region point end))))
(setq this-command 'delete-region))
;;;###autoload
(defun prot-simple-delete-line-backward ()
"Delete (not kill) from point to the beginning of the line."
(interactive)
(let* ((point (point))
(beg (line-beginning-position))
(beg- (- beg 1)))
(cond
((< beg- (point-min)) (delete-region beg point))
((= point beg) (delete-region beg- point))
(t (delete-region beg point))))
(setq this-command 'delete-region))
;;;###autoload
(define-minor-mode prot-simple-auto-fill-visual-line-mode
"Enable `visual-line-mode' and disable `auto-fill-mode' in the current buffer."
:global nil
(if prot-simple-auto-fill-visual-line-mode
(progn
(auto-fill-mode -1)
(visual-line-mode 1))
(auto-fill-mode 1)
(visual-line-mode -1)))
(kill-line 0))
;;;; Commands for text insertion or manipulation
@ -448,7 +272,7 @@ to `line-beginning-position'."
(line-end-position) :no-error
(prefix-numeric-value current-prefix-arg))
(line-beginning-position))))
(when-let* ((regexp-end (prot-simple--pos-url-on-line char)))
(when-let ((regexp-end (prot-simple--pos-url-on-line char)))
(goto-char regexp-end)
(unless (looking-at ">")
(insert ">")
@ -525,7 +349,7 @@ demarcated by BEG and END."
;; `prot-simple-insert-line-prefix'."
;; :type '(repeat string)
;; :group 'prot-simple)
;;
;;
;; (defun prot-simple--line-prefix-regexp (&optional string)
;; "Format regular expression for `prot-simple--line-prefix-p'.
;; With optional STRING use it directly. Else format the regexp by
@ -533,36 +357,36 @@ demarcated by BEG and END."
;; (if string
;; (format "^%s " string)
;; (format "^[%s] " (apply #'concat prot-simple-line-prefix-strings))))
;;
;;
;; (defun prot-simple--line-prefix-p (&optional string)
;; "Return non-nil if line beginning has an appropriate string prefix.
;; With optional STRING test that it is at the beginning of the line."
;; (save-excursion
;; (goto-char (line-beginning-position))
;; (looking-at (prot-simple--line-prefix-regexp string))))
;;
;;
;; (defun prot-simple--line-prefix-insert (string)
;; "Insert STRING at the beginning of the line, followed by a space."
;; (save-excursion
;; (goto-char (line-beginning-position))
;; (insert string)
;; (insert " ")))
;;
;;
;; (defun prot-simple--line-prefix-infer-string ()
;; "Return line prefix string if it matches `prot-simple--line-prefix-p'."
;; (when (prot-simple--line-prefix-p)
;; (string-trim
;; (buffer-substring-no-properties (match-beginning 0) (match-end 0)))))
;;
;;
;; (defun prot-simple--line-prefix-toggle (string)
;; "Insert or remove STRING at the beginning of the line."
;; (if (prot-simple--line-prefix-p string)
;; (delete-region (match-beginning 0) (match-end 0))
;; (prot-simple--line-prefix-insert string)))
;;
;;
;; (defvar prot-simple--line-prefix-history nil
;; "Minibuffer history of `prot-simple--line-prefix-prompt'.")
;;
;;
;; (defun prot-simple--line-prefix-prompt ()
;; "Prompt for string to use as line prefix.
;; Provide `prot-simple-line-prefix-strings' as completion
@ -573,30 +397,30 @@ demarcated by BEG and END."
;; prot-simple-line-prefix-strings
;; nil nil nil
;; 'prot-simple--line-prefix-history default)))
;;
;;
;; (defun prot-simple-line-prefix-infer-or-prompt ()
;; "Infer string for line prefix or prompt for one."
;; (or (prot-simple--line-prefix-infer-string)
;; (prot-simple--line-prefix-prompt)))
;;
;;
;; ;;;###autoload
;; (defun prot-simple-insert-line-prefix-dwim (string)
;; "Toggle presence of STRING at the beginning of the line.
;;
;;
;; When called interactively try to infer STRING based on the line
;; prefix. If one is found among `prot-simple-line-prefix-strings',
;; perform a removal outright.
;;
;;
;; If no string can be inferred, prompt for STRING among
;; `prot-simple-line-prefix-strings'. Accept arbitrary strings at
;; the prompt.
;;
;;
;; When the region is active, toggle the presence of STRING for each
;; line in the region."
;; (interactive (list (prot-simple-line-prefix-infer-or-prompt)))
;; (if-let* ((region-p (region-active-p))
;; (beg (region-beginning))
;; (end (line-number-at-pos (region-end))))
;; (if-let ((region-p (region-active-p))
;; (beg (region-beginning))
;; (end (line-number-at-pos (region-end))))
;; (progn
;; (goto-char beg)
;; (push-mark (point))
@ -616,11 +440,11 @@ demarcated by BEG and END."
(end (pos-eol))
diff-eol-point
diff-eol-mark)
(when-let* (((use-region-p))
(pos (point))
(mrk (mark))
(line-diff-mark-point (1+ (- (line-number-at-pos mrk)
(line-number-at-pos pos)))))
(when-let (((use-region-p))
(pos (point))
(mrk (mark))
(line-diff-mark-point (1+ (- (line-number-at-pos mrk)
(line-number-at-pos pos)))))
(if (> pos mrk)
(setq start (pos-bol line-diff-mark-point)) ; pos-bol of where the mark is
(setq end (pos-eol line-diff-mark-point))) ; pos-eol of the line where the mark is
@ -647,15 +471,15 @@ demarcated by BEG and END."
(defun prot-simple--move-line-user-error (boundary)
"Return `user-error' with message accounting for BOUNDARY.
BOUNDARY is a buffer position, expected to be `point-min' or `point-max'."
(when-let* ((bound (line-number-at-pos boundary))
(scope (cond
((and (use-region-p)
(or (= (line-number-at-pos (point)) bound)
(= (line-number-at-pos (mark)) bound)))
"region is ")
((= (line-number-at-pos (point)) bound)
"")
(t nil))))
(when-let ((bound (line-number-at-pos boundary))
(scope (cond
((and (use-region-p)
(or (= (line-number-at-pos (point)) bound)
(= (line-number-at-pos (mark)) bound)))
"region is ")
((= (line-number-at-pos (point)) bound)
"")
(t nil))))
(user-error (format "Warning: %salready in the last line!" scope))))
(defun prot-simple-move-above-dwim (arg)
@ -704,39 +528,27 @@ There is no dragging the character forward. This is the
behaviour of `transpose-chars' when point is at the end of the
line."
(interactive)
(if (eq (point) (line-end-position))
(transpose-chars 1)
(transpose-chars -1)
(forward-char)))
(transpose-chars -1)
(forward-char))
;;;; Commands for paragraphs
;;;###autoload
(defun prot-simple-unfill-region-or-paragraph ()
"Unfill current paragraph or the active region."
(interactive)
(unless mark-ring ; needed when entering a new buffer
(push-mark (point) t nil))
(defun prot-simple-unfill-region-or-paragraph (&optional beg end)
"Unfill paragraph or, when active, the region.
Join all lines in region delimited by BEG and END, if active,
while respecting any empty lines (so multiple paragraphs are not
joined, just unfilled). If no region is active, operate on the
paragraph. The idea is to produce the opposite effect of both
`fill-paragraph' and `fill-region'."
(interactive "r")
(let ((fill-column most-positive-fixnum))
(if (region-active-p)
(fill-region (region-beginning) (region-end))
(if (use-region-p)
(fill-region beg end)
(fill-paragraph))))
;;;; Commands for windows and pages
;;;###autoload
(defun prot-simple-other-window ()
"Wrapper for `other-window' and `next-multiframe-window'.
If there is only one window and multiple frames, call
`next-multiframe-window'. Otherwise, call `other-window'."
(interactive)
(if (and (one-window-p) (length> (frame-list) 1))
(progn
(call-interactively #'next-multiframe-window)
(setq this-command #'next-multiframe-window))
(call-interactively #'other-window)
(setq this-command #'other-window)))
;;;###autoload
(defun prot-simple-narrow-visible-window ()
"Narrow buffer to wisible window area.
@ -846,56 +658,13 @@ Name the buffer after the defun's symbol."
;;;; Commands for buffers
(defun prot-simple--display-unsaved-buffers (buffers buffer-menu-name)
"Produce buffer menu listing BUFFERS called BUFFER-MENU-NAME."
(let ((old-buf (current-buffer))
(buf (get-buffer-create buffer-menu-name)))
(with-current-buffer buf
(Buffer-menu-mode)
(setq-local Buffer-menu-files-only nil
Buffer-menu-buffer-list buffers
Buffer-menu-filter-predicate nil)
(list-buffers--refresh buffers old-buf)
(tabulated-list-print))
(display-buffer buf)))
(defun prot-simple--get-unsaved-buffers ()
"Get list of unsaved buffers."
(seq-filter
(lambda (buffer)
(and (buffer-file-name buffer)
(buffer-modified-p buffer)))
(buffer-list)))
;;;###autoload
(defun prot-simple-display-unsaved-buffers ()
"Produce buffer menu listing unsaved file-visiting buffers."
(defun prot-simple-other-windor-or-frame ()
"Switch to other window or frame.
If there is only one window, call `other-frame'. Otherwise use
`other-window'."
(interactive)
(if-let* ((unsaved-buffers (prot-simple--get-unsaved-buffers)))
(prot-simple--display-unsaved-buffers unsaved-buffers "*Unsaved buffers*")
(message "No unsaved buffers")))
(defun prot-simple-display-unsaved-buffers-on-exit (&rest _)
"Produce buffer menu listing unsaved file-visiting buffers.
Add this as :before advice to `save-buffers-kill-emacs'."
(when-let* ((unsaved-buffers (prot-simple--get-unsaved-buffers)))
(prot-simple--display-unsaved-buffers unsaved-buffers "*Unsaved buffers*")))
;;;###autoload
(defun prot-simple-copy-current-buffer-name ()
"Add the current buffer's name to the `kill-ring'."
(declare (interactive-only t))
(interactive)
(kill-new (buffer-name (current-buffer))))
;;;###autoload
(defun prot-simple-copy-current-buffer-file ()
"Add the current buffer's file path to the `kill-ring'."
(declare (interactive-only t))
(interactive)
(if buffer-file-name
(kill-new buffer-file-name)
(user-error "%s is not associated with a file" (buffer-name (current-buffer)))))
(call-interactively (if (one-window-p) #'other-frame #'other-window)))
;;;###autoload
(defun prot-simple-kill-buffer (buffer)
@ -905,13 +674,8 @@ When called interactively, prompt for BUFFER."
(let ((kill-buffer-query-functions nil))
(kill-buffer (or buffer (current-buffer)))))
(make-obsolete
'prot-simple-kill-buffer-current
'prot-simple-kill-buffer-dwim
"2025-11-04")
;;;###autoload
(defun prot-simple-kill-buffer-dwim (&optional arg)
(defun prot-simple-kill-buffer-current (&optional arg)
"Kill current buffer.
With optional prefix ARG (\\[universal-argument]) delete the
buffer's window as well. Kill the window regardless of ARG if it
@ -919,31 +683,11 @@ satisfies `prot-common-window-small-p' and it has no previous
buffers in its history."
(interactive "P")
(let ((kill-buffer-query-functions nil))
(cond
;; Is a tab whose last window's buffer is to be deleted.
((and (one-window-p)
(length> (tab-bar-tabs) 1))
(let ((buffer (current-buffer)))
(tab-close)
(kill-buffer buffer)))
;; Is an ancillary window that appeared for this buffer but is
;; otherwise not supposed to be there.
((and (prot-common-window-small-p)
(null (window-prev-buffers)))
(kill-buffer-and-window))
(t
(kill-buffer)))))
;;;###autoload
(defun prot-simple-copy-current-path ()
"Copy the current absolute path.
If in a file-visiting buffer, copy the variable `buffer-file-name'.
Else copy the `default-directory'."
(declare (interactive-only t))
(interactive)
(let ((path (or buffer-file-name default-directory)))
(kill-new path)
(message "Copied `%s'" (propertize path 'face 'success))))
(if (or (and (prot-common-window-small-p)
(null (window-prev-buffers)))
(and arg (not (one-window-p))))
(kill-buffer-and-window)
(kill-buffer))))
;;;###autoload
(defun prot-simple-rename-file-and-buffer (name)
@ -1047,135 +791,5 @@ VARIANT is either `dark' or `light'."
(interactive (list (prot-simple--list-accessible-colors-prompt)))
(list-colors-display (prot-simple-accessible-colors variant)))
(defun prot-simple-update-package-repositories-pull (package package-directory buffer)
"Pull PACKAGE which extends PACKAGE-DIRECTORY.
Use BUFFER for standard output and return the exit code."
(let ((default-directory package-directory))
(message "Pulling %s from directory %s" package default-directory)
(call-process "git" nil (list buffer t) nil "pull")))
(defun prot-simple-update-package-repositories-clone (package base-directory buffer)
"Clone PACKAGE to an extension of BASE-DIRECTORY.
Use BUFFER for standard output and return the exit code."
(message "Cloning %s to directory %s" package base-directory)
(call-process "git" nil (list buffer t) nil "clone" (format "git@github.com:protesilaos/%s %s" package base-directory)))
(define-error 'prot-package-no-update "Package could not be updated" 'error)
(defun prot-simple-update-package-repositories-subr (packages)
"Pull or clone all repositories of my PACKAGES."
(unless (executable-find "git")
(user-error "Cannot find git program; install it first or add it to the $PATH; aborting"))
(unless (getenv "SSH_AUTH_SOCK")
(user-error "Cannot find $SSH_AUTH_SOCK; check your SSH connection; aborting"))
(let ((stdout (get-buffer-create " *prot-simple-git-package-stdout*")))
(dolist (package packages)
(let* ((name (cond
((symbolp package) (symbol-name package))
((stringp package) package)
(t (error "The `%s' is neither a symbol nor a string" package))))
(common-directory (expand-file-name "~/Git/Projects/"))
(package-directory (expand-file-name name common-directory))
(exit-code (if (file-directory-p package-directory)
(prot-simple-update-package-repositories-pull package package-directory stdout)
(prot-simple-update-package-repositories-clone package common-directory stdout))))
(condition-case error-data
(when (> exit-code 0)
(signal 'prot-package-no-update (list (format "Package `%s' got exit code `%s'" package exit-code))))
(:success
(message "Updated `%s' repository" package))
(prot-package-no-update
;; TODO 2025-11-06: Is it safe to stash changes outright?
;; I think it is fine, but maybe there is a case where this
;; can lead to data loss?
(when (file-directory-p package-directory)
(let ((default-directory package-directory))
(unless (fboundp 'vc-git-stash)
(require 'vc-git))
(vc-git-stash (format "prot-package-update %s: %s" (format-time-string "%FT%T") package))))
(message "Tried to stash changes in package `%s' because: %s" package (cdr error-data)))
((error user-error)
(message "The package `%s' returned error data: %s" package error-data))
(quit
(message "Aborted by the user")))))))
(defvar prot-simple-update-package-repositories-prompt-history nil
"Minibuffer history of `prot-simple-update-package-repositories-prompt'.")
(defun prot-simple-update-package-repositories-prompt ()
"Prompt for packages among `prot-emacs-my-packages'."
(let ((default (car prot-simple-update-package-repositories-prompt-history)))
(completing-read-multiple
(format-prompt "Select packages" default)
prot-emacs-my-packages
nil t nil
'prot-simple-update-package-repositories-prompt-history
default)))
;;;###autoload
(defun prot-simple-update-some-or-all-of-my-package-repositories (packages &optional all-packages)
"Prompt for PACKAGES among `prot-emacs-my-packages' to pull or clone.
With a universal prefix argument for ALL-PACKAGES, do not prompt for packages and
update them all instead."
(interactive
(list
(if current-prefix-arg
prot-emacs-my-packages
(prot-simple-update-package-repositories-prompt))))
(prot-simple-update-package-repositories-subr packages))
;;;; Global minor mode to override keymaps
(defvar prot-simple-override-mode-map (make-sparse-keymap)
"keymap for `prot-simple-override-mode'.
Enable that mode to have its key bindings take effect over those of the
major mode.")
(define-minor-mode prot-simple-override-mode
"Enable the `prot-simple-override-mode-map'."
:init-value nil
:global t
:keymap prot-simple-override-mode-map)
;;;; Preview hexadecimal RGB colours
;; FIXME 2026-01-10: Why is this not working in Org tables? The
;; properties are applied but the face is not displayed.
(defun prot-simple-hex-fontify (limit)
"Fontify the # of a hexadecimal RGB colour up to LIMIT."
(when-let* ((found (re-search-forward "\\(#\\)\\([a-fA-F0-9]\\{6\\}\\)" limit t))
(color (match-string-no-properties 0))
(beg (match-beginning 1))
(end (match-end 1))
(face (list :background color :foreground color :inverse-video t)))
(add-text-properties beg end (list 'face face 'font-lock-face face 'display "######"))
found))
(defun prot-simple-hex-unfontify ()
"Fontify the # of a hexadecimal RGB colour up to LIMIT."
(save-excursion
(goto-char (point-min))
(while-let ((found (re-search-forward "\\(#\\)\\([a-fA-F0-9]\\{6\\}\\)" nil t))
(color (match-string-no-properties 0))
(beg (match-beginning 1))
(end (match-end 1))
(face (list :background color :foreground color :inverse-video t)))
(put-text-property beg end 'display "#")
found)))
(defvar prot-simple-hex-color-keywords
'((prot-simple-hex-fontify))
"Font lock keywords for `prot-simple-hex-color-mode'.")
(define-minor-mode prot-simple-hex-color-mode
"Preview hexadecimal colour values."
:global nil
:init-value nil
(if prot-simple-hex-color-mode
(font-lock-add-keywords nil prot-simple-hex-color-keywords)
(prot-simple-hex-unfontify)
(font-lock-remove-keywords nil prot-simple-hex-color-keywords))
(font-lock-flush (point-min) (point-max)))
(provide 'prot-simple)
;;; prot-simple.el ends here

View file

@ -1,8 +1,8 @@
;;; prot-spell.el --- Spelling-related extensions for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2021-2026 Protesilaos
;; Copyright (C) 2021-2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -26,6 +26,11 @@
;;
;; This covers my spelling-related extensions, for use in my Emacs
;; setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
@ -68,19 +73,15 @@ Used by `prot-spell-change-dictionary'."
;;;###autoload
(defun prot-spell-spell-dwim (beg end)
"Spell check between BEG END, current word, or select a dictionary.
"Spellcheck between BEG END, current word, or select dictionary.
Use `flyspell-region' on the active region and deactivate the
mark.
With point over a word and no active region invoke `ispell-word'.
Else call `prot-spell-change-dictionary'."
Use `flyspell-region' on the active region. With point over a
word and no active region invoke `ispell-word'. Else call
`prot-spell-change-dictionary'."
(interactive "r")
(cond
((use-region-p)
(flyspell-region beg end)
(deactivate-mark))
(flyspell-region beg end))
((thing-at-point 'word)
(call-interactively 'ispell-word))
(t
@ -96,42 +97,5 @@ function. Then you can control the buffer's specifics via
(advice-add #'ispell-display-buffer :override #'prot-spell-ispell-display-buffer)
;;;; French punctuation
;; TODO 2026-07-01: Maybe extend the command to check for all sorts of
;; rules, in accordance with the Charte orthotypographique du Journal
;; officiel:
;; <https://www.legifrance.gouv.fr/contenu/Media/Files/autour-de-la-loi/legislatif-et-reglementaire/charte_typographique_jo_janvier_2021.pdf?__cf_chl_f_tk=YcOBi.sMMWmrN5FxCQMM8YDz.OyTC62mXKuZnjZ2rvs-1782901834-1.0.1.1-IzZmLEEkwksUf1sSm5dljuFqSZjDPjXfLMYkIdbOa.Y>.
;;
;; Though that is not a simple search+replace operation...
;;
;; For the following:
;;
;; «Lespace insécable permet déviter la séparation entre deux
;; éléments. Elle sutilisera, entre autres, à lintérieur des
;; guillemets français, au sein des nombres, après les dito, avec les
;; tirets dincise.»
(defvar prot-spell-french-grammar-replacements
'(("\\([^[:punct:][:space:]]\\)\\?" . "\\1 ?")
("\\([^[:punct:][:space:]]\\):" . "\\1 :")
("\\([^[:punct:][:space:]]\\);" . "\\1 ;")
("\\([^[:punct:][:space:]]\\)!" . "\\1 !")
("«\\([^[:punct:][:space:]]\\)" . "« \\1")
("\\([^[:punct:][:space:]]\\" . "\\1 »")
("\\([^[:punct:][:space:]]\\)%" . "\\1 %"))
"Add no-break space to relevant punctuation.")
;;;###autoload
(defun prot-spell-french-punctuation ()
"Apply the `prot-spell-french-grammar-replacements' across the buffer."
(interactive)
(save-excursion
(save-restriction
(widen)
(pcase-dolist (`(,target . ,replacement) prot-spell-french-grammar-replacements)
(goto-char (point-min))
(while (re-search-forward target nil t)
(replace-match replacement))))))
(provide 'prot-spell)
;;; prot-spell.el ends here

View file

@ -1,80 +0,0 @@
;;; prot-streaming.el --- Tweaks for when I do videos or share my screen -*- lexical-binding: t -*-
;; Copyright (C) 2026 Protesilaos
;; Author: Protesilaos <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; This covers my streaming-related extensions, for use in my Emacs
;; setup: https://protesilaos.com/emacs/dotemacs.
;;; Code:
(require 'display-line-numbers)
(require 'hl-line)
(defcustom prot-streaming-mode-modes
'(text-mode prog-mode dired-mode comint-mode)
"Major modes where `prot-streaming-mode' takes effect.")
(defun prot-streaming-mode--display-line-numbers (arg)
"Call `display-line-numbers-mode' with ARG.
Do it for all buffers whose major mode is one among `prot-streaming-mode-modes'."
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when (derived-mode-p prot-streaming-mode-modes)
(display-line-numbers-mode arg)))))
(defun prot-streaming--get-mode-hooks ()
"Return hooks belonging to `prot-streaming-mode-modes'."
(mapcar
(lambda (symbol)
(intern (format "%s-hook" symbol)))
prot-streaming-mode-modes))
(defun prot-streaming-mode--affect-hooks (add-or-remove)
"Apply ADD-OR-REMOVE to `prot-streaming-mode-hooks'.
ADD-OR-REMOVE is the keyword `:add' or `:remove', which correspond to
`add-hook' and `remove-hook'."
(let ((fn (pcase add-or-remove
(:add #'add-hook)
(:remove #'remove-hook)
(_ (error "ADD-OR-REMOVE must be either `:add' or `:remove'")))))
(dolist (hook (prot-streaming--get-mode-hooks))
(funcall fn hook #'display-line-numbers-mode))))
;;;###autoload
(define-minor-mode prot-streaming-mode
"Enable extras for streaming."
:global t
:init-value nil
(if prot-streaming-mode
(progn
(prot-streaming-mode--display-line-numbers 1)
(prot-streaming-mode--affect-hooks :add)
(global-hl-line-mode 1))
(prot-streaming-mode--display-line-numbers -1)
(prot-streaming-mode--affect-hooks :remove)
(global-hl-line-mode -1)))
(provide 'prot-streaming)
;;; prot-streaming.el ends here

View file

@ -1,8 +1,8 @@
;;; prot-vertico.el --- Custom Vertico extras -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Protesilaos
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -23,6 +23,11 @@
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
@ -35,8 +40,7 @@
:prompt ""
:separator ""
:ellipsis ""
:no-match ""))
(vertico-preselect . prompt))
:no-match "")))
"List of configurations for minimal Vertico multiform.
The minimal view is intended to be more private or less
revealing. This is important when, for example, a prompt shows
@ -49,29 +53,38 @@ command or use the commands `prot-vertico-private-next' and
automatically.")
(defvar prot-vertico-multiform-maximal
'(reverse
(vertico-count . 10)
(vertico-preselect . directory)
'((vertico-count . 10)
(vertico-resize . t))
"List of configurations for maximal Vertico multiform.")
(defun prot-vertico--match-directory (str)
"Match directory delimiter in STR."
(string-suffix-p "/" str))
;; From the Vertico documentation.
(defun prot-vertico-sort-directories-first (files)
"Sort directories before FILES."
(setq files (vertico-sort-alpha files))
(nconc (seq-filter #'prot-vertico--match-directory files)
(seq-remove #'prot-vertico--match-directory files)))
(defun prot-vertico-private-next ()
"Like `vertico-next' but toggle reverse vertical view if needed.
"Like `vertico-next' but toggle vertical view if needed.
This is done to accommodate `prot-vertico-multiform-minimal'."
(interactive)
(if vertico-unobtrusive-mode
(progn
(vertico-multiform-reverse)
(let ((vertico--index 0))
(vertico-multiform-vertical)
(vertico-next 1))
(vertico-next 1)))
(defun prot-vertico-private-previous ()
"Like `vertico-previous' but toggle reverse vertical view if needed.
"Like `vertico-previous' but toggle vertical view if needed.
This is done to accommodate `prot-vertico-multiform-minimal'."
(interactive)
(if vertico-unobtrusive-mode
(progn
(vertico-multiform-reverse)
(vertico-multiform-vertical)
(vertico-previous 1))
(vertico-previous 1)))
@ -82,43 +95,8 @@ This is done to accommodate `prot-vertico-multiform-minimal'."
(if (and vertico-unobtrusive-mode (> vertico--total 1))
(progn
(minibuffer-complete)
(prot-vertico-private-next))
(vertico-multiform-vertical))
(vertico-insert)))
(defun prot-vertico-private-exit ()
"Exit with the candidate if `prot-vertico-multiform-minimal'.
If there are more candidates that match the given input, expand the
minibuffer to show the remaining candidates and select the first one.
Else do `vertico-exit'."
(interactive)
(cond
((and (= vertico--total 1)
(not (eq 'file (vertico--metadata-get 'category))))
(minibuffer-complete)
(vertico-exit))
((and vertico-unobtrusive-mode
(not minibuffer--require-match)
(or (string-empty-p (minibuffer-contents))
minibuffer-default
(eq vertico-preselect 'directory)
(eq vertico-preselect 'prompt)))
(vertico-exit-input))
((and vertico-unobtrusive-mode (> vertico--total 1))
(minibuffer-complete-and-exit)
(prot-vertico-private-next))
(t
(vertico-exit))))
;; (cl-defgeneric vertico--display-candidates (lines)
;; "Reverse the default vertico view of LINES."
;; (move-overlay vertico--candidates-ov (point-min) (point-min))
;; ;; (setq lines (nreverse lines))
;; (unless (eq vertico-resize t)
;; (setq lines (nconc (make-list (max 0 (- vertico-count (length lines))) "\n") lines)))
;; (let ((string (apply #'concat lines)))
;; (add-face-text-property 0 (length string) 'default 'append string)
;; (overlay-put vertico--candidates-ov 'before-string string)
;; (overlay-put vertico--candidates-ov 'after-string nil)))
(provide 'prot-vertico)
;;; prot-vertico.el ends here

View file

@ -1,8 +1,8 @@
;;; prot-window.el --- Display-buffer and window-related extensions for my dotemacs -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Protesilaos
;; Copyright (C) 2023 Protesilaos Stavrou
;; Author: Protesilaos <info@protesilaos.com>
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.1"))
@ -26,6 +26,11 @@
;;
;; This covers my window and display-buffer extensions, for use in my
;; Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes. I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;; Code:
@ -43,7 +48,7 @@ number (integer or floating point) or a function.")
(defun prot-window--get-window-size (key)
"Extract the value of KEY from `prot-window-window-sizes'."
(when-let* ((value (plist-get prot-window-window-sizes key)))
(when-let ((value (plist-get prot-window-window-sizes key)))
(cond
((functionp value)
(funcall value))
@ -52,7 +57,7 @@ number (integer or floating point) or a function.")
(t
(error "The value of `%s' is neither a number nor a function" key)))))
(defun prot-window-select-fit-size (window)
(defun prot-window-select-fit-size (window &rest _)
"Select WINDOW and resize it.
The resize pertains to the maximum and minimum values for height
and width, per `prot-window-window-sizes'.
@ -64,13 +69,7 @@ Use this as the `body-function' in a `display-buffer-alist' entry."
(prot-window--get-window-size :max-height)
(prot-window--get-window-size :min-height)
(prot-window--get-window-size :max-width)
(prot-window--get-window-size :min-width))
;; If we did not use `display-buffer-below-selected', then we must
;; be in a lateral window, which has more space. Then we do not
;; want to dedicate the window to this buffer, because we will be
;; running out of space.
(when (or (window-in-direction 'above) (window-in-direction 'below))
(set-window-dedicated-p window t)))
(prot-window--get-window-size :min-width)))
(defun prot-window--get-display-buffer-below-or-pop ()
"Return list of functions for `prot-window-display-buffer-below-or-pop'."
@ -96,21 +95,6 @@ This as the action function in a `display-buffer-alist' entry."
(when (apply fn args)
(throw 'success fn))))))
(defun prot-window-display-new (buffer alist)
"Display buffer in a new window if possible."
(when-let* ((root (frame-root-window))
(window (ignore-errors
(or (split-window-horizontally nil root)
(split-window-below nil root)))))
(window--display-buffer buffer window 'window alist)))
(defun prot-window-display-other-or-current (&rest args)
"Display buffer in the current or other window.
Use the other window if only one window is present."
(if (length> (window-list) 1)
(apply #'display-buffer-same-window args)
(apply #'display-buffer-pop-up-window args)))
(defun prot-window-shell-or-term-p (buffer &rest _)
"Check if BUFFER is a shell or terminal.
This is a predicate function for `buffer-match-p', intended for
@ -121,18 +105,6 @@ use in `display-buffer-alist'."
(and (not (derived-mode-p 'message-mode 'text-mode))
(derived-mode-p 'eshell-mode 'shell-mode 'comint-mode 'fundamental-mode)))))
(defun prot-window-remove-dedicated (&rest _)
"Remove dedicated window parameter.
Use this as :after advice to `delete-other-windows' and
`delete-window'."
(when (one-window-p :no-mini)
(set-window-dedicated-p nil nil)))
(mapc
(lambda (fn)
(advice-add fn :after #'prot-window-remove-dedicated))
'(delete-other-windows delete-window))
(defmacro prot-window-define-full-frame (name &rest args)
"Define command to call ARGS in new frame with `display-buffer-full-frame' bound.
Name the function prot-window- followed by NAME. If ARGS is nil,
@ -158,9 +130,9 @@ call NAME as a function."
(defun prot-window--get-new-shell-buffer ()
"Return buffer name for `shell' buffers."
(if-let* ((buffers (prot-window--get-shell-buffers))
(buffers-length (length buffers))
((>= buffers-length 1)))
(if-let ((buffers (prot-window--get-shell-buffers))
(buffers-length (length buffers))
((>= buffers-length 1)))
(format "*shell*<%s>" (1+ buffers-length))
"*shell*"))
@ -169,7 +141,7 @@ call NAME as a function."
(let ((name (prot-window--get-new-shell-buffer)))
(shell name)
(set-frame-name name)
(when-let* ((buffer (get-buffer name)))
(when-let ((buffer (get-buffer name)))
(with-current-buffer buffer
(add-hook
'delete-frame-functions
@ -183,24 +155,20 @@ call NAME as a function."
nil
:local)))))
;;;###autoload (autoload 'prot-window-meeting "prot-window")
(prot-window-define-full-frame meeting
(let ((buffer (get-buffer-create "*scratch for meeting*")))
;;;###autoload (autoload 'prot-window-coach "prot-window")
(prot-window-define-full-frame coach
(let ((buffer (get-buffer-create "*scratch for coach*")))
(with-current-buffer buffer
(funcall initial-major-mode)
(when (and (zerop (buffer-size))
(stringp initial-scratch-message))
(insert initial-scratch-message)))
(funcall initial-major-mode))
(display-buffer buffer)
(set-frame-name "Meeting")))
(set-frame-name "Coach")))
;; REVIEW 2023-06-25: Does this merit a user option? I don't think I
;; will ever set it to the left. It feels awkward there.
(defun prot-window-scroll-bar-placement ()
"Control the placement of scroll bars."
(when scroll-bar-mode
(setq default-frame-scroll-bars 'right)
(set-scroll-bar-mode 'right)))
(setq default-frame-scroll-bars 'right)
(set-scroll-bar-mode 'right))
(add-hook 'scroll-bar-mode-hook #'prot-window-scroll-bar-placement)
@ -210,59 +178,5 @@ call NAME as a function."
(add-hook 'after-make-frame-functions 'prot-window-no-minibuffer-scroll-bar)
;;;; Run commands in a popup frame (via emacsclient)
(defun prot-window-delete-popup-frame (&rest _)
"Kill selected selected frame if it has parameter `prot-window-popup-frame'.
Use this function via a hook."
(dolist (frame (frame-list))
(when (frame-parameter frame 'prot-window-popup-frame)
(delete-frame frame))))
;; NOTE 2025-02-11: Also see `prot-vertico-with-buffer-mode', which
;; extends this to use a full-frame buffer for Vertico.
(defmacro prot-window-define-with-popup-frame (command)
"Define function which calls COMMAND in a new frame.
Make the new frame have the `prot-window-popup-frame' parameter."
`(defun ,(intern (format "prot-window-popup-%s" command)) ()
,(format "Run `%s' in a popup frame with `prot-window-popup-frame' parameter.
Also see `prot-window-delete-popup-frame'." command)
(interactive)
(let ((frame (make-frame '((prot-window-popup-frame . t)
(explicit-name . t)
(name . "prot-window-popup")))))
(select-frame frame)
(switch-to-buffer " prot-window-hidden-buffer-for-popup-frame")
(condition-case nil
(call-interactively ',command)
((quit error user-error)
(delete-frame frame))))))
(declare-function org-capture "org-capture" (&optional goto keys))
(declare-function tmr "tmr" (time &optional description acknowledgep))
(declare-function prot-project-switch "prot-project" (directory))
;;;###autoload (autoload 'prot-window-popup-org-capture "prot-window")
(prot-window-define-with-popup-frame org-capture) ; defines command `prot-window-popup-org-capture'
;;;###autoload (autoload 'prot-window-popup-tmr "prot-window")
(prot-window-define-with-popup-frame tmr) ; defines command `prot-window-popup-tmr'
;;;###autoload (autoload 'prot-window-popup-prot-project-switch "prot-window")
(prot-window-define-with-popup-frame prot-project-switch) ; defines command `prot-window-popup-prot-project-switch'
(defun prot-window-set-delete-popup-hook (feature hook)
"Set up `prot-window-delete-popup-frame' for FEATURE with HOOK."
(with-eval-after-load feature
(add-hook hook #'prot-window-delete-popup-frame)))
(defvar org-capture-after-finalize-hook)
(defvar tmr-timer-created-functions)
(defvar prot-project-switch-hook)
(prot-window-set-delete-popup-hook 'org-capture 'org-capture-after-finalize-hook)
(prot-window-set-delete-popup-hook 'tmr 'tmr-timer-created-functions)
(prot-window-set-delete-popup-hook 'prot-project 'prot-project-switch-hook)
(provide 'prot-window)
;;; prot-window.el ends here

View file

@ -1,21 +0,0 @@
[Desktop Entry]
Name=Prot Emacsclient
GenericName=Text Editor
Comment=Edit text
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;x-scheme-handler/org-protocol;
Exec=sh -c "if [ -n \\"\\$*\\" ]; then exec /usr/local/bin/emacsclient --alternate-editor="emacs" --display=\\"\\$DISPLAY\\" \\"\\$@\\"; else exec /usr/local/bin/emacsclient --alternate-editor="emacs" --create-frame; fi" sh %F
Icon=emacs
Type=Application
Terminal=false
Categories=Development;TextEditor;
StartupWMClass=Emacs
Keywords=emacsclient;
Actions=new-window;new-instance;
[Desktop Action new-window]
Name=New Window
Exec=/usr/local/bin/emacsclient --alternate-editor="emacs" --create-frame %F
[Desktop Action new-instance]
Name=New Instance
Exec=emacs %F

View file

@ -7,11 +7,11 @@
<description>Set substitute fonts for Latin</description>
<!-- Aporetic is my custom build of Iosevka: <https://github.com/protesilaos/aporetic> -->
<!-- Iosevka Comfy is my custom build of Iosevka: <https://git.sr.ht/~protesilaos/iosevka-comfy> -->
<!-- Serif -->
<alias>
<family>Aporetic Serif</family>
<family>Iosevka Comfy Motion Duo</family>
<default><family>serif</family></default>
</alias>
<alias>
@ -29,7 +29,7 @@
<!-- Sans Serif -->
<alias>
<family>Aporetic Sans</family>
<family>Iosevka Comfy Duo</family>
<default><family>sans-serif</family></default>
</alias>
<alias>
@ -47,7 +47,8 @@
<!-- Monospaced -->
<alias>
<family>Aporetic Serif Mono</family>
<!-- We need the "fixed" variant for programs such as XTerm -->
<family>Iosevka Comfy Motion Fixed</family>
<default><family>monospace</family></default>
</alias>
<alias>

View file

@ -7,12 +7,12 @@
<description>Set preferable fonts for Latin</description>
<!-- Aporetic is my custom build of Iosevka: <https://github.com/protesilaos/aporetic> -->
<!-- Iosevka Comfy is my custom build of Iosevka: <https://git.sr.ht/~protesilaos/iosevka-comfy> -->
<alias binding="strong">
<family>serif</family>
<prefer>
<family>Aporetic Serif</family>
<family>Iosevka Comfy Motion Duo</family>
<family>Noto Serif</family>
<family>Liberation Serif</family>
</prefer>
@ -23,7 +23,7 @@
<alias binding="strong">
<family>sans-serif</family>
<prefer>
<family>Aporetic Sans</family>
<family>Iosevka Comfy Duo</family>
<family>FiraGO</family>
<family>Roboto</family>
<family>Noto Sans</family>
@ -36,7 +36,8 @@
<alias binding="strong">
<family>monospace</family>
<prefer>
<family>Aporetic Serif Mono</family>
<family>Iosevka Comfy Motion Fixed</family>
<!-- My custom build of Hack: <https://git.sr.ht/~protesilaos/hack-font-mod> -->
<family>Hack</family>
<family>Source Code Pro</family>
</prefer>

View file

@ -1,159 +0,0 @@
#!/bin/bash
# gnome_custom_keys.sh --- My custom key bindings for GNOME
#
# Copyright (C) 2025-2026 Protesilaos <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
### Commentary:
#
# My custom key bindings for GNOME. This may not be complete but it
# fixes all the known keybinding-related conflicts I encounter
# whenever I want to use Emacs inside of GNOME.
#
# Part of my dotfiles: <https://github.com/protesilaos/dotfiles>.
### Code:
gsettings set org.gnome.TextEditor keybindings 'default'
gsettings set org.gnome.desktop.wm.keybindings activate-window-menu "['']"
gsettings set org.gnome.desktop.wm.keybindings always-on-top "['']"
gsettings set org.gnome.desktop.wm.keybindings begin-move "['']"
gsettings set org.gnome.desktop.wm.keybindings begin-resize "['']"
gsettings set org.gnome.desktop.wm.keybindings close "['<Super>q']"
gsettings set org.gnome.desktop.wm.keybindings cycle-group "['']"
gsettings set org.gnome.desktop.wm.keybindings cycle-group-backward "['']"
gsettings set org.gnome.desktop.wm.keybindings cycle-panels "['']"
gsettings set org.gnome.desktop.wm.keybindings cycle-panels-backward "['']"
gsettings set org.gnome.desktop.wm.keybindings cycle-windows "['']"
gsettings set org.gnome.desktop.wm.keybindings cycle-windows-backward "['']"
gsettings set org.gnome.desktop.wm.keybindings lower "['']"
gsettings set org.gnome.desktop.wm.keybindings maximize "['']"
gsettings set org.gnome.desktop.wm.keybindings maximize-horizontally "['']"
gsettings set org.gnome.desktop.wm.keybindings maximize-vertically "['']"
gsettings set org.gnome.desktop.wm.keybindings minimize "['<Super>h']"
gsettings set org.gnome.desktop.wm.keybindings move-to-center "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-corner-ne "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-corner-nw "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-corner-se "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-corner-sw "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-monitor-down "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-monitor-left "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-monitor-right "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-monitor-up "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-side-e "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-side-n "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-side-s "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-side-w "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-1 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-10 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-11 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-12 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-2 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-3 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-4 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-5 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-6 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-7 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-8 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-9 "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-down "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-last "['']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-left "['<Shift><Super>Left']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-right "['<Shift><Super>Right']"
gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-up "['']"
gsettings set org.gnome.desktop.wm.keybindings panel-main-menu "['']"
gsettings set org.gnome.desktop.wm.keybindings panel-run-dialog "['']"
gsettings set org.gnome.desktop.wm.keybindings raise "['']"
gsettings set org.gnome.desktop.wm.keybindings raise-or-lower "['']"
gsettings set org.gnome.desktop.wm.keybindings set-spew-mark "['']"
gsettings set org.gnome.desktop.wm.keybindings show-desktop "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-applications "['<Super>Tab']"
gsettings set org.gnome.desktop.wm.keybindings switch-applications-backward "['<Shift><Super>Tab']"
gsettings set org.gnome.desktop.wm.keybindings switch-group "['<Super>grave']"
gsettings set org.gnome.desktop.wm.keybindings switch-group-backward "['<Shift><Super>grave']"
gsettings set org.gnome.desktop.wm.keybindings switch-input-source "['<Super>space', 'XF86Keyboard']"
gsettings set org.gnome.desktop.wm.keybindings switch-input-source-backward "['<Shift><Super>space', '<Shift>XF86Keyboard']"
gsettings set org.gnome.desktop.wm.keybindings switch-panels "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-panels-backward "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-1 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-10 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-11 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-12 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-2 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-3 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-4 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-5 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-6 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-7 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-8 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-9 "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-down "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-last "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-left "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-right "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-up "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-windows "['<Alt>Tab']"
gsettings set org.gnome.desktop.wm.keybindings switch-windows-backward "['<Shift><Alt>Tab']"
gsettings set org.gnome.desktop.wm.keybindings toggle-above "['']"
gsettings set org.gnome.desktop.wm.keybindings toggle-fullscreen "['<Super>f']"
gsettings set org.gnome.desktop.wm.keybindings toggle-maximized "['<Super>m']"
gsettings set org.gnome.desktop.wm.keybindings toggle-on-all-workspaces "['']"
gsettings set org.gnome.desktop.wm.keybindings toggle-shaded "['']"
gsettings set org.gnome.desktop.wm.keybindings unmaximize "['']"
gsettings set org.gnome.metacity.keybindings toggle-tiled-left "['<Super>Left']"
gsettings set org.gnome.metacity.keybindings toggle-tiled-right "['<Super>Right']"
gsettings set org.gnome.mutter.keybindings rotate-monitor "['']"
gsettings set org.gnome.mutter.keybindings switch-monitor "['']"
gsettings set org.gnome.mutter.keybindings tab-popup-cancel "['']"
gsettings set org.gnome.mutter.keybindings tab-popup-select "['']"
gsettings set org.gnome.mutter.keybindings toggle-tiled-left "['<Super>Left']"
gsettings set org.gnome.mutter.keybindings toggle-tiled-right "['<Super>Right']"
gsettings set org.gnome.mutter.wayland.keybindings restore-shortcuts "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-1 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-10 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-11 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-12 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-2 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-3 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-4 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-5 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-6 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-7 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-8 "['']"
gsettings set org.gnome.mutter.wayland.keybindings switch-to-session-9 "['']"
gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings "['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/', '/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/']"
gsettings set org.gnome.shell.keybindings focus-active-notification "['']"
gsettings set org.gnome.shell.keybindings open-application-menu "['']"
gsettings set org.gnome.shell.keybindings screenshot "['<Shift>Print']"
gsettings set org.gnome.shell.keybindings screenshot-window "['<Alt>Print']"
gsettings set org.gnome.shell.keybindings shift-overview-down "['']"
gsettings set org.gnome.shell.keybindings shift-overview-up "['']"
gsettings set org.gnome.shell.keybindings show-screen-recording-ui "['']"
gsettings set org.gnome.shell.keybindings show-screenshot-ui "['Print']"
gsettings set org.gnome.shell.keybindings switch-to-application-1 "['']"
gsettings set org.gnome.shell.keybindings switch-to-application-2 "['']"
gsettings set org.gnome.shell.keybindings switch-to-application-3 "['']"
gsettings set org.gnome.shell.keybindings switch-to-application-4 "['']"
gsettings set org.gnome.shell.keybindings switch-to-application-5 "['']"
gsettings set org.gnome.shell.keybindings switch-to-application-6 "['']"
gsettings set org.gnome.shell.keybindings switch-to-application-7 "['']"
gsettings set org.gnome.shell.keybindings switch-to-application-8 "['']"
gsettings set org.gnome.shell.keybindings switch-to-application-9 "['']"
gsettings set org.gnome.shell.keybindings toggle-application-view "['<Super>a']"
gsettings set org.gnome.shell.keybindings toggle-message-tray "['']"
gsettings set org.gnome.shell.keybindings toggle-overview "['<Super>d']"
gsettings set org.gtk.Settings.Debug enable-inspector-keybinding false
gsettings set org.gtk.gtk4.Settings.Debug enable-inspector-keybinding true

View file

@ -1,13 +0,0 @@
<?xml version="1.0"?>
<!DOCTYPE wallpapers SYSTEM "gnome-wp-list.dtd">
<wallpapers>
<wallpaper deleted="false">
<name>Default Prot Background</name>
<filename>/home/prot/Pictures/light.jpg</filename>
<filename-dark>/home/prot/Pictures/dark.jpg</filename-dark>
<options>zoom</options>
<shade_type>solid</shade_type>
<pcolor>#80c6bb</pcolor>
<scolor>#000000</scolor>
</wallpaper>
</wallpapers>

View file

@ -1,3 +0,0 @@
Use this:
systemctl --user enable dirmngr.socket gpg-agent.socket gpg-agent-ssh.socket

View file

@ -1,3 +0,0 @@
VteTerminal, vte-terminal {
padding: 15px 30px;
}

View file

@ -1,3 +0,0 @@
VteTerminal, vte-terminal {
padding: 15px 30px;
}

View file

@ -1,3 +0,0 @@
To enable mbsync with notmuch integration, do the following:
systemctl --user enable mbsync.timer

View file

@ -1,37 +0,0 @@
#!/bin/bash
# mbsync_remove_uid.sh --- Remove the UID from email files.
#
# Copyright (C) 2025-2026 Protesilaos <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
### Commentary:
#
# Remove the mbsync UID from email files. THIS IS RISKY SO PLEASE MAKE
# SURE YOU UNDERSTAND WHAT YOU ARE DOING.
#
# Part of my dotfiles: <https://github.com/protesilaos/dotfiles>.
### Code:
my_mail_directory="$HOME"/.mail
while IFS= read -r -d $'\0' file
do
if [ -f "$file" ]
then
mv "$file" "${file//,U=[0-9]*:/:}"
fi
done < <(find "$my_mail_directory" -type f -iname "*,U=[0-9]*" -print0)

View file

@ -1,89 +1,82 @@
# Gandi
####################################################################################################
IMAPAccount gandi
IMAPAccount pub
Host mail.gandi.net
UserCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prot-gandi/ { print $(NF-2); exit; }'"
PassCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prot-gandi/ { print $NF; exit; }'"
TLSType IMAPS
UserCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/pub/ { print $(NF-2); exit; }'"
PassCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/pub/ { print $NF; exit; }'"
SSLType IMAPS
IMAPStore gandi-remote
Account gandi
IMAPStore pub-remote
Account pub
MaildirStore gandi-local
MaildirStore pub-local
Subfolders Verbatim
# The trailing "/" in 'Path' is important. The path given to 'Path'
# must exist, but the path given to 'Inbox' is created automatically
# by 'mbsync'.
Path ~/.mail/gandi/
Inbox ~/.mail/gandi/Inbox
# The trailing "/" is important
Path ~/.mail/pub/
Inbox ~/.mail/pub/Inbox
Channel gandi
Far :gandi-remote:
Near :gandi-local:
Channel pub
Far :pub-remote:
Near :pub-local:
# Include everything
Patterns *
Sync All
Create Near
Create Both
Remove Both
Expunge Both
SyncState *
# NOTE 2024-12-01: I do not use the others, but have tested them in
# the past. I keep the settings here in case I decide to test the
# functionality again.
##########
# # Gmail
# ####################################################################################################
# IMAPAccount gmail
# Host imap.gmail.com
# UserCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $(NF-2); exit; }'"
# PassCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $NF; exit; }'"
# SSLType IMAPS
#
# IMAPStore gmail-remote
# Account gmail
#
# MaildirStore gmail-local
# Subfolders Verbatim
# # The trailing "/" is important
# Path ~/.mail/gmail/
# Inbox ~/.mail/gmail/Inbox
#
# Channel gmail
# Far :gmail-remote:
# Near :gmail-local:
# # Include everything
# Patterns *
# Sync All
# Create Near
# Remove Near
# Expunge Near
# SyncState *
IMAPAccount inf
Host mail.gandi.net
UserCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/inf/ { print $(NF-2); exit; }'"
PassCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/inf/ { print $NF; exit; }'"
SSLType IMAPS
IMAPStore inf-remote
Account inf
MaildirStore inf-local
Subfolders Verbatim
# The trailing "/" is important
Path ~/.mail/inf/
Inbox ~/.mail/inf/Inbox
Channel inf
Far :inf-remote:
Near :inf-local:
# Include everything
Pattern *
Sync All
Create Both
Remove Both
Expunge Both
SyncState *
##########
IMAPAccount prv
Host mail.gandi.net
UserCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prv/ { print $(NF-2); exit; }'"
PassCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prv/ { print $NF; exit; }'"
SSLType IMAPS
IMAPStore prv-remote
Account prv
MaildirStore prv-local
Subfolders Verbatim
# The trailing "/" is important
Path ~/.mail/prv/
Inbox ~/.mail/prv/Inbox
Channel prv
Far :prv-remote:
Near :prv-local:
# Include everything
Patterns *
Sync All
Create Both
Remove Both
Expunge Both
SyncState *
# # Mailbox
# ####################################################################################################
# IMAPAccount mailbox
# Host imap.mailbox.org
# UserCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $(NF-2); exit; }'"
# PassCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $NF; exit; }'"
# SSLType IMAPS
#
# IMAPStore mailbox-remote
# Account mailbox
#
# MaildirStore mailbox-local
# Subfolders Verbatim
# # The trailing "/" is important
# Path ~/.mail/mailbox/
# Inbox ~/.mail/mailbox/Inbox
#
# Channel mailbox
# Far :mailbox-remote:
# Near :mailbox-local:
# # Include everything
# Patterns *
# Sync All
# Create Near
# Remove Near
# Expunge Near
# SyncState *

View file

@ -1,133 +0,0 @@
MBTN_LEFT ignore
MBTN_LEFT_DBL ignore
MBTN_RIGHT ignore
MBTN_BACK ignore
MBTN_FORWARD ignore
WHEEL_UP ignore
WHEEL_DOWN ignore
WHEEL_LEFT ignore
WHEEL_RIGHT ignore
RIGHT seek 5 # seek 5 seconds forward
LEFT seek -5 # seek 5 seconds backward
UP seek 60 # seek 1 minute forward
DOWN seek -60 # seek 1 minute backward
Shift+RIGHT ignore
Shift+LEFT ignore
Shift+UP ignore
Shift+DOWN ignore
Ctrl+LEFT ignore
Ctrl+RIGHT ignore
Ctrl+Shift+LEFT ignore
Ctrl+Shift+RIGHT ignore
Alt+left ignore
Alt+right ignore
Alt+up ignore
Alt+down ignore
Alt++ ignore
Alt+- ignore
Alt+BS ignore
PGUP ignore
PGDWN ignore
Shift+PGUP ignore
Shift+PGDWN ignore
[ ignore
] ignore
{ ignore
} ignore
BS ignore
Shift+BS ignore
Shift+Ctrl+BS ignore
q ignore
Q ignore
q ignore
ESC ignore
ESC ignore
p ignore
. ignore
, ignore
SPACE cycle pause # toggle pause/playback mode
> ignore
ENTER ignore
< ignore
O ignore
o ignore
P ignore
i ignore
I ignore
` ignore
z ignore
Z ignore
x ignore
ctrl++ ignore
ctrl+- ignore
Shift+g ignore
Shift+f ignore
9 ignore
/ ignore
0 ignore
* ignore
m ignore
1 ignore
2 ignore
3 ignore
4 ignore
5 ignore
6 ignore
7 ignore
8 ignore
Alt+0 ignore
Alt+1 ignore
Alt+2 ignore
d ignore
r ignore
R ignore
t ignore
v ignore
Alt+v ignore
V ignore
u ignore
j ignore
J ignore
SHARP ignore
_ ignore
T ignore
f ignore
s ignore
S ignore
Ctrl+s ignore
Alt+s ignore
w ignore
W ignore
e ignore
A ignore
POWER ignore
PLAY ignore
PAUSE ignore
PLAYPAUSE ignore
PLAYONLY ignore
PAUSEONLY ignore
STOP ignore
FORWARD ignore
REWIND ignore
NEXT ignore
PREV ignore
VOLUME_UP ignore
VOLUME_DOWN ignore
MUTE ignore
CLOSE_WIN quit
ctrl+w ignore
E ignore
l ignore
L ignore
ctrl+c ignore
DEL ignore
ctrl+h ignore
F8 ignore
F9 ignore
! ignore
@ ignore
? ignore
? ignore

View file

@ -1,43 +1,36 @@
# See my mbsync config, which is reflected here.
# https://github.com/protesilaos/dotfiles
# See my mbsync config, which is reflectd here.
# https://git.sr.ht/~protesilaos/dotfiles
defaults
auth on
protocol smtp
tls on
tls_starttls on
# Gandi
####################################################################################################
account gandi
# FIXME 2023-08-08: I cannot send the message if `tls_starttls' is on.
# Why?
tls_starttls off
host mail.gandi.net
port 587
eval echo from $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prot-gandi/ { print $(NF-2); exit; }')
eval echo user $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prot-gandi/ { print $(NF-2); exit; }')
passwordeval "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prot-gandi/ { print $NF; exit; }'"
port 465
# NOTE 2024-12-01: I do not use the others, but have tested them in
# the past. I keep the settings here in case I decide to test the
# functionality again.
# pub
account pub
# FIXME 2023-08-08: Optimise this?
eval echo from $(gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/pub/ { print $(NF-2); exit; }')
eval echo user $(gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/pub/ { print $(NF-2); exit; }')
passwordeval "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/pub/ { print $NF; exit; }'"
# # Gmail
# ####################################################################################################
# account gmail
# host smtp.gmail.com
# port 587
# eval echo from $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $(NF-2); exit; }')
# eval echo user $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $(NF-2); exit; }')
# passwordeval "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $NF; exit; }'"
# # Mailbox
# ####################################################################################################
# account mailbox
# host smtp.mailbox.org
# port 465
# eval echo from $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $(NF-2); exit; }')
# eval echo user $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $(NF-2); exit; }')
# passwordeval "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $NF; exit; }'"
# inf
account inf
# FIXME 2023-08-08: Optimise this?
eval echo from $(gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/inf/ { print $(NF-2); exit; }')
eval echo user $(gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/inf/ { print $(NF-2); exit; }')
passwordeval "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/inf/ { print $NF; exit; }'"
# prv
account prv
# FIXME 2023-08-08: Optimise this?
eval echo from $(gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prv/ { print $(NF-2); exit; }')
eval echo user $(gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prv/ { print $(NF-2); exit; }')
passwordeval "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prv/ { print $NF; exit; }'"
# Set a default account (I copied from man msmtp)
account default : gandi
account default : pub

View file

@ -1,8 +1,8 @@
#!/bin/bash
#!/bin/sh
# post-new --- Notmuch rules that run after notmuch-new(1)
# post-new --- Notmuch rules that run after notmuch new
#
# Copyright (C) 2021-2026 Protesilaos <info@protesilaos.com>
# Copyright (c) 2021-2023 Protesilaos Stavrou <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -17,26 +17,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
### Commentary:
## Commentary:
#
# Note that the order of those commands matters.
# The order of those commands matters. Maybe I will write something
# more sophisticated in the future.
#
# Part of my dotfiles: <https://github.com/protesilaos/dotfiles>.
# Part of my dotfiles: <https://git.sr.ht/~protesilaos/dotfiles>.
### Code:
# remove "unread" from "replied"
notmuch tag -unread -new -- tag:replied
# tag all "new" messages "inbox" and "unread"
while IFS= read -r -d $'\0' file
do
if [[ "$file" != *R* ]]; then
notmuch tag +unread -- "path:${file}"
fi
done < <(notmuch search --output=files --format=text0 tag:new)
notmuch tag +inbox -new -- tag:new
notmuch tag +inbox +unread -new -- tag:new
# tag my replies as "sent"
# notmuch tag -new -unread +inbox +sent -- '(from:"*@protesilaos.com*" not to:"*@protesilaos.com*" not tag:list)'
@ -51,4 +44,19 @@ notmuch tag +coach -- date:2023-01-20..now subject:/[Tt]utor\|[Cc]oach\|[Ll]esso
# Another "mostly okay" but not 100% correct way of identifying my
# package-related messages.
notmuch tag +package -- "(to:protesilaos/*noreply.github.com or to:*incoming.gitlab.com) not tag:archived not tag:list"
notmuch tag +package -- "(to:~protesilaos/*@lists.sr.ht or to:protesilaos/*noreply.github.com or to:*incoming.gitlab.com) not tag:archived not tag:list"
# mailing lists
## emacs
notmuch tag -inbox +list +emacs -- from:emacs-devel@gnu.org or to:emacs-devel@gnu.org
notmuch tag -inbox +list +emacs -- from:emacs-orgmode@gnu.org or to:emacs-orgmode@gnu.org
notmuch tag -inbox +list +emacs -- 'to:"/*@debbugs.gnu.org*/"'
notmuch tag -inbox +list +emacs -- from:emacs-humanities@gnu.org or to:emacs-humanities@gnu.org not to:emacs-humanities-owner@gnu.org
## Others
notmuch tag -inbox +list +notmuch -- from:notmuch@notmuchmail.org or to:notmuch@notmuchmail.org
notmuch tag -inbox +list +sourcehut -- 'to:"/*~sircmpwn/sr.ht-discuss@lists.sr.ht*/"'
# include mailing lists sent to me into the "inbox"
notmuch tag +inbox -- '(to:"*@protesilaos.com*" tag:list)'

View file

@ -1,90 +0,0 @@
#!/bin/bash
# pre-new --- Notmuch rules that run before notmuch-new(1)
#
# Copyright (C) 2021-2026 Protesilaos <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
### Commentary:
#
# Move unwanted email to the appropriate local directory and tag it
# accordingly. The order of those commands matters.
#
# Part of my dotfiles: <https://github.com/protesilaos/dotfiles>.
### Code:
if [ "$HOSTNAME" = "kronos" ]
then
mail_root_path=$(notmuch config get database.path)
for tag in del spam
do
target_directory="${mail_root_path}/${tag}local/"
if [ ! -d "$target_directory" ]
then
mkdir -p "$target_directory"
fi
while IFS= read -r -d $'\0' file
do
if [ -f "$file" ]
then
mv "$file" "$target_directory$(basename -- "${file//,U=[0-9]*:/:}")"
fi
done < <(notmuch search --output=files --format=text0 tag:"$tag")
notmuch tag -inbox -"${tag}" +"${tag}"local -- tag:"$tag"
done
fi
### PROT 2025-03-01: To archive mail manually, I did the following.
### This is useful once every few years, so there is no need to
### automate it. Note that this assumes that mbsync can remove and
### expunge mail on the far/server side.
#
# Confirm that we have messages matching these search terms:
#
# notmuch count tag:"sent" from:*@protesilaos.com date:..2024-01-01
#
# Now add a tag to all of them to make it clear they are archived:
#
# notmuch tag +archivesent -sent -- tag:"sent" from:*@protesilaos.com date:..2024-01-01
#
# Confirm the count is the same for the new tag:
#
# notmuch count tag:"archivesent"
#
# Same principle for the inbox:
#
# notmuch count tag:"inbox" date:..2024-01-01
# notmuch tag +archiveinbox -inbox -- tag:"inbox" date:..2024-01-01
# notmuch count tag:"archiveinbox"
#
# Now move the messages to their respective directories, which I
# created beforehand (note that I use `echo` to make sure it works
# before doing anything---remove the `echo` to actually move the
# files).
#
# while IFS= read -r -d $'\0' file; do echo mv "$file" "$HOME"/.mail/inboxlocal/$(basename -- "${file//,U=[0-9]*:/:}") ; done < <(notmuch search --output=files --format=text0 tag:"archiveinbox" and not tag:"archivesent")
#
# while IFS= read -r -d $'\0' file; do echo mv "$file" "$HOME"/.mail/sentlocal/$(basename -- "${file//,U=[0-9]*:/:}") ; done < <(notmuch search --output=files --format=text0 tag:"archivesent")
#
# Then do an `mbsync` followed by `notmuch new` to synchronise everything.
#
# In case the aforementioned tags are confusing:
#
# notmuch tag -archiveinbox +iarchive -- tag:archiveinbox
# notmuch tag -archivesent +isent -- tag:archivesent

View file

@ -31,9 +31,9 @@ path=/home/prot/.mail
#
[user]
name=Protesilaos
primary_email=prot@protesilaos.com
other_email=public@protesilaos.com;info@protesilaos.com
name=Protesilaos Stavrou
primary_email=public@protesilaos.com
other_email=info@protesilaos.com
# Configuration for "notmuch new"
#
@ -52,7 +52,7 @@ other_email=public@protesilaos.com;info@protesilaos.com
[new]
tags=new
ignore=.uidvalidity;.mbsyncstate;.mbsyncstate.new;.mbsyncstate.lock
ignore=.uidvalidity;.mbsyncstate
# Search configuration
#
@ -65,7 +65,7 @@ ignore=.uidvalidity;.mbsyncstate;.mbsyncstate.new;.mbsyncstate.lock
#
[search]
exclude_tags=
exclude_tags=del;spam;
# Maildir compatibility configuration
#

View file

@ -0,0 +1,11 @@
[Desktop Entry]
Version=1.0
Name=Redshift
Exec=redshift-gtk
Icon=redshift
Terminal=false
Type=Application
Categories=Utility;
StartupNotify=true
Hidden=false
X-GNOME-Autostart-enabled=true

View file

@ -1,113 +0,0 @@
configuration {
modi: "run,drun,window";
font: "Monospace 10";
location: 0; /* Centre of the screen. See the themes for width, borders, etc. */
fixed-num-lines: true;
show-icons: true;
terminal: "uxterm";
/* run-command: "{cmd}";*/
/* run-list-command: "";*/
/* run-shell-command: "{terminal} -e {cmd}";*/
/* window-command: "xkill -id {window}";*/
window-match-fields: "all";
drun-match-fields: "name,exec";
case-sensitive: true;
cycle: true;
sidebar-mode: false;
/* eh: 1;*/
auto-select: false;
combi-modi: "window,drun";
matching: "regex";
/* tokenize: true;*/
/* m: "-5";*/
/* filter: ;*/
/* dpi: -1;*/
threads: 0;
/* scroll-method: 0;*/
window-format: "{w:0} {n:0} {t:0}";
click-to-exit: true;
max-history-size: 25;
combi-hide-mode-prefix: true;
/* pid: "/run/user/1000/rofi.pid";*/
display-window: "Windows";
/* display-windowcd: ;*/
display-run: "Executables";
/* display-ssh: ;*/
display-drun: "Applications";
display-combi: "Combined";
/* display-keys: ;*/
kb-primary-paste: "Control+Y";
kb-secondary-paste: "Control+y";
kb-clear-line: "Control+Shift+BackSpace";
kb-move-front: "Control+a";
kb-move-end: "Control+e";
kb-move-word-back: "Alt+b";
kb-move-word-forward: "Alt+f";
kb-move-char-back: "Left,Control+b";
kb-move-char-forward: "Right,Control+f";
kb-remove-word-back: "Control+BackSpace";
kb-remove-word-forward: "Alt+d";
kb-remove-char-forward: "Delete,Control+d";
kb-remove-char-back: "BackSpace";
kb-remove-to-eol: "Control+k";
kb-remove-to-sol: "Control+K";
kb-accept-entry: "Control+j,Control+m,Return,KP_Enter";
/* kb-accept-custom: "Control+Return";*/
/* kb-accept-alt: "Shift+Return";*/
/* kb-delete-entry: "Shift+Delete";*/
/* kb-mode-next: "Shift+Right,Control+Tab";*/
/* kb-mode-previous: "Shift+Left,Control+ISO_Left_Tab";*/
/* kb-row-left: "Control+Page_Up";*/
/* kb-row-right: "Control+Page_Down";*/
/* kb-row-up: "Up,Control+p,ISO_Left_Tab";*/
/* kb-row-down: "Down,Control+n";*/
/* kb-row-tab: "Tab";*/
kb-page-prev: "Alt+v,Page_Up";
kb-page-next: "Control+v,Page_Down";
kb-row-first: "Alt+less,Home,KP_Home";
kb-row-last: "Alt+greater,End,KP_End";
/* kb-row-select: "Control+space";*/
/* kb-screenshot: "Alt+S";*/
/* kb-toggle-case-sensitivity: "grave,dead_grave";*/
/* kb-toggle-sort: "Alt+grave";*/
/* kb-cancel: "Escape,Control+g,Control+bracketleft";*/
/* kb-custom-1: "Alt+1";*/
/* kb-custom-2: "Alt+2";*/
/* kb-custom-3: "Alt+3";*/
/* kb-custom-4: "Alt+4";*/
/* kb-custom-5: "Alt+5";*/
/* kb-custom-6: "Alt+6";*/
/* kb-custom-7: "Alt+7";*/
/* kb-custom-8: "Alt+8";*/
/* kb-custom-9: "Alt+9";*/
/* kb-custom-10: "Alt+0";*/
/* kb-custom-11: "Alt+exclam";*/
/* kb-custom-12: "Alt+at";*/
/* kb-custom-13: "Alt+numbersign";*/
/* kb-custom-14: "Alt+dollar";*/
/* kb-custom-15: "Alt+percent";*/
/* kb-custom-16: "Alt+dead_circumflex";*/
/* kb-custom-17: "Alt+ampersand";*/
/* kb-custom-18: "Alt+asterisk";*/
/* kb-custom-19: "Alt+parenleft";*/
/* kb-select-1: "Super+1";*/
/* kb-select-2: "Super+2";*/
/* kb-select-3: "Super+3";*/
/* kb-select-4: "Super+4";*/
/* kb-select-5: "Super+5";*/
/* kb-select-6: "Super+6";*/
/* kb-select-7: "Super+7";*/
/* kb-select-8: "Super+8";*/
/* kb-select-9: "Super+9";*/
/* kb-select-10: "Super+0";*/
/* ml-row-left: "ScrollLeft";*/
/* ml-row-right: "ScrollRight";*/
/* ml-row-up: "ScrollUp";*/
/* ml-row-down: "ScrollDown";*/
/* me-select-entry: "MousePrimary";*/
/* me-accept-entry: "MouseDPrimary";*/
/* me-accept-custom: "Control+MouseDPrimary";*/
}
/* Rofi version 1.7.2 */
@theme "~/.config/rofi/themes/modus-themes-active.rasi"

View file

@ -1,34 +0,0 @@
/**
* rofi -dump-theme output.
* Rofi version: 1.6.1
* User: Protesilaos
* Copyright: Protesilaos --- Released under the GNU GPLv3
* Comment: Simple port of the Modus Operandi theme for Emacs (https://protesilaos.com/modus-themes)
**/
* {
/* Custom colours */
bg-alt: #e0e0e0ff;
bg-main: #ffffffff;
fg-alt: #595959ff;
fg-main: #000000ff;
red: #a60000ff;
green: #006800ff;
yellow: #6f5500ff;
blue: #0031a9ff;
magenta: #721045ff;
cyan: #005e8bff;
red-bright: #b60000ff;
green-bright: #008900ff;
yellow-bright: #808000ff;
blue-bright: #0000ffff;
magenta-bright: #dd22ddff;
cyan-bright: #008899ff;
selection-bg: #c0deffff;
/* Global properties */
background-color: rgba (0,0,0, 0%);
highlight: bold underline #1f1fce;
}
@import "modus-themes-common.rasi"

View file

@ -1 +0,0 @@
@import "modus-operandi.rasi"

View file

@ -1,147 +0,0 @@
/**
* rofi -dump-theme output.
* Rofi version: 1.6.1
* User: Protesilaos
* Copyright: Protesilaos --- Released under the GNU GPLv3
* Comment: Simple port of the Modus themes for Emacs (https://protesilaos.com/modus-themes)
**/
window {
background-color: @bg-main;
border: 5px;
border-color: @bg-alt;
padding: 10px;
width: 33%;
height: 8%;
/* max-height: 25%; */
}
mainbox {
border: 0;
padding: 0;
}
message {
border: 2px solid 0px 0px;
border-color: @fg-alt;
padding: 1px ;
}
textbox {
text-color: @fg-main;
}
listview {
fixed-height: 0;
border: 2px solid 0px 0px;
border-color: @bg-alt;
spacing: 4px;
scrollbar: true;
padding: 2px 0px 0px;
}
element {
border: 0;
padding: 1px;
}
element.normal.normal {
background-color: @bg-main;
text-color: @fg-main;
}
element.normal.urgent {
background-color: @red;
text-color: @bg-alt;
}
element.normal.active {
background-color: @bg-alt;
text-color: @fg-alt;
}
element.selected.normal {
background-color: @selection-bg;
text-color: @fg-main;
}
element.selected.urgent {
background-color: @red-bright;
text-color: @bg-main;
}
element.selected.active {
background-color: @blue;
text-color: @bg-main;
}
element.alternate.normal {
background-color: @bg-main;
text-color: @fg-main;
}
element.alternate.urgent {
background-color: @red-bright;
text-color: @bg-alt;
}
element alternate.active {
background-color: @cyan-bright;
text-color: @bg-alt;
}
mode-switcher {
border: 2px solid 0px 0px ;
border-color: @bg-alt;
}
scrollbar {
/* width: 4px; */
width: 0;
border: 0;
handle-color: @fg-alt;
/* handle-width: 8px; */
handle-width: 0;
padding: 0;
}
sidebar {
border: 2px solid 0px 0px;
border-color: @fg-alt;
}
button {
spacing: 0;
background-color: @bg-main;
text-color: @fg-alt;
padding: 5px;
horizontal-align: 0.5;
}
button.selected {
background-color: @bg-alt;
text-color: @fg-main;
}
inputbar {
spacing: 10;
padding: 1px;
text-color: @fg-main;
children: [ prompt,entry ];
}
prompt {
spacing: 0;
text-color: @cyan;
}
entry {
spacing: 0;
text-color: @fg-main;
}
element-text {
background-color: inherit;
text-color: inherit;
}

View file

@ -1,34 +0,0 @@
/**
* rofi -dump-theme output.
* Rofi version: 1.6.1
* User: Protesilaos
* Copyright: Protesilaos --- Released under the GNU GPLv3
* Comment: Simple port of the Modus Operandi theme for Emacs (https://protesilaos.com/modus-themes)
**/
* {
/* Custom colours */
bg-alt: #303030ff;
bg-main: #000000ff;
fg-alt: #989898ff;
fg-main: #ffffffff;
red: #ff5f59ff;
green: #44bc44ff;
yellow: #d0bc00ff;
blue: #2fafffff;
magenta: #feacd0ff;
cyan: #00d3d0ff;
red-bright: #ff5f5fff;
green-bright: #44df44ff;
yellow-bright: #efef00ff;
blue-bright: #338fffff;
magenta-bright: #ff66ffff;
cyan-bright: #00eff0ff;
selection-bg: #2f447fff;
/* Global properties */
background-color: rgba (0,0,0, 0%);
highlight: bold underline #4fafff;
}
@import "modus-themes-common.rasi"

View file

@ -1,7 +1,7 @@
#
# ~/.bash_profile
#
# Part of my dotfiles: https://github.com/protesilaos/dotfiles
# Part of my dotfiles: https://git.sr.ht/~protesilaos/dotfiles
#
# Load my configs

View file

@ -4,7 +4,7 @@
#
# My Bash configuration file. Any modules such as the `dircolors`
# config are stored in ~/.local/share/my_bash/. This setup is part of
# my dotfiles. See https://github.com/protesilaos/dotfiles.
# my dotfiles. See https://git.sr.ht/~protesilaos/dotfiles.
#
# Note that ALL MY SCRIPTS are designed to work with `#!/bin/bash`.
# They are not tested for portability.
@ -18,8 +18,14 @@ _checkexec ()
### General settings
# Set PATH so it includes user's private executables.
if [ -d "$HOME"/.local/bin ]; then
# Include my scripts in the PATH.
if [ -d "$HOME"/bin ]
then
PATH=$PATH:"$HOME"/bin
fi
if [ -d "$HOME"/.local/bin ]
then
PATH=$PATH:"$HOME"/.local/bin
fi
@ -51,11 +57,30 @@ fi
# something like ~/.profile.
export SSH_AUTH_SOCK="/run/user/$(id -u)/gnupg/S.gpg-agent.ssh"
# Default pager. The check for the terminal is useful for Emacs with
# M-x shell (which is how I usually interact with bash these days).
#
# The COLORTERM is documented in (info "(emacs) General Variables").
# I found the reference to `dumb-emacs-ansi' in (info "(emacs)
# Connection Variables").
if [ "$TERM" = "dumb" ] && [ "$INSIDE_EMACS" ]
then
export PAGER="cat"
alias less="cat"
export TERM=dumb-emacs-ansi
export COLORTERM=1
else
# Quit once you try to scroll past the end of the file.
export PAGER="less --quit-at-eof"
fi
export MANPAGER="$PAGER"
# Default editor.
if pgrep -x emacs > /dev/null
then
export EDITOR="emacsclient -a emacs -t -r"
export VISUAL="emacsclient -a emacs -t -r"
export VISUAL="emacsclient -c"
export EDITOR="emacsclient -t"
elif _checkexec gvim
then
export VISUAL="gvim"
@ -65,31 +90,6 @@ else
export EDITOR=$VISUAL
fi
# Default pager. The check for the terminal is useful for Emacs with
# M-x shell (which is how I usually interact with bash these days).
#
# The COLORTERM is documented in (info "(emacs) General Variables").
# I found the reference to `dumb-emacs-ansi' in (info "(emacs)
# Connection Variables").
if [ "$TERM" = "dumb" ] && [ "$INSIDE_EMACS" ] || [ "$TERM" = "dumb-emacs-ansi" ] && [ "$INSIDE_EMACS" ]
then
PAGER="cat"
alias less="cat"
TERM=dumb-emacs-ansi
COLORTERM=1
# I always enable 'server-mode' from inside Emacs, so this
# connects to an existing frame. It creates a new frame if the
# current one is too small. It starts a new Emacs instance if
# there is no server running.
EDITOR="emacsclient -a emacs -t -r"
VISUAL="emacsclient -a emacs -t -r"
else
# Quit once you try to scroll past the end of the file.
export PAGER="less --quit-at-eof"
fi
export MANPAGER="$PAGER"
# Default browser. This leverages the MIME list.
export BROWSER=/usr/bin/xdg-open
@ -152,17 +152,6 @@ _checkexec lesspipe && eval "$(SHELL=/bin/sh lesspipe)"
# remember the original command when necessary. There are some
# exceptions for commands I seldom execute.
#### Apt (Debian)
if _checkexec apt
then
alias aptuu="sudo apt update && sudo apt upgrade"
alias aptar="sudo apt autoremove"
alias aptss="apt search -n"
alias aptsh="apt show"
alias aptls="dpkg -L"
fi
#### Pacman and Yay (Arch Linux)
if _checkexec pacman
@ -274,7 +263,7 @@ fi
if _checkexec yt-dlp
then
alias ytaud='yt-dlp --add-metadata -ci --extract-audio --audio-format mp3 -o "%(title)s.%(ext)s"'
alias ytvid='yt-dlp --add-metadata --no-playlist --no-part --write-description --newline --prefer-free-formats -o "%(title)s.%(ext)s"'
alias ytvid='yt-dlp --add-metadata --no-playlist --no-part --write-description --newline --prefer-free-formats -o "%(title)s.%(ext)s" '
fi
# Just for fun.

View file

@ -2,12 +2,12 @@
# Then I modified it heavily. For Emacs in particular, the M-x shell
# benefits from the inclusion of `TERM dumb-emacs-ansi'. Check my
# .bashrc for the relevant entry. Everything is part of my dotfiles:
# <https://github.com/protesilaos/dotfiles>.
# <https://git.sr.ht/~protesilaos/dotfiles>.
# Configuration file for dircolors, a utility to help you set the
# LS_COLORS environment variable used by GNU ls with the --color option.
# Copyright (C) 1996-2026 Free Software Foundation, Inc.
# Copyright (C) 1996-2023 Free Software Foundation, Inc.
# Copying and distribution of this file, with or without modification,
# are permitted provided the copyright notice and this notice are preserved.
# The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the

View file

@ -0,0 +1,66 @@
Socket-activated dirmngr and gpg-agent with systemd
===================================================
When used on a GNU/Linux system supervised by systemd, you can ensure
that the GnuPG daemons dirmngr and gpg-agent are launched
automatically the first time they're needed, and shut down cleanly at
session logout. This is done by enabling user services via
socket-activation.
System distributors
-------------------
The *.service and *.socket files (from this directory) should be
placed in /usr/lib/systemd/user/ alongside other user-session services
and sockets.
To enable socket-activated dirmngr for all accounts on the system,
use:
systemctl --user --global enable dirmngr.socket
To enable socket-activated gpg-agent for all accounts on the system,
use:
systemctl --user --global enable gpg-agent.socket
Additionally, you can enable socket-activated gpg-agent ssh-agent
emulation for all accounts on the system with:
systemctl --user --global enable gpg-agent-ssh.socket
You can also enable restricted ("--extra-socket"-style) gpg-agent
sockets for all accounts on the system with:
systemctl --user --global enable gpg-agent-extra.socket
Individual users
----------------
A user on a system with systemd where this has not been installed
system-wide can place these files in ~/.config/systemd/user/ to make
them available.
If a given service isn't installed system-wide, or if it's installed
system-wide but not globally enabled, individual users will still need
to enable them. For example, to enable socket-activated dirmngr for
all future sessions:
systemctl --user enable dirmngr.socket
To enable socket-activated gpg-agent with ssh support, do:
systemctl --user enable gpg-agent.socket gpg-agent-ssh.socket
These changes won't take effect until your next login after you've
fully logged out (be sure to terminate any running daemons before
logging out).
If you'd rather try a socket-activated GnuPG daemon in an
already-running session without logging out (with or without enabling
it for all future sessions), kill any existing daemon and start the
user socket directly. For example, to set up socket-activated dirmgnr
in the current session:
gpgconf --kill dirmngr
systemctl --user start dirmngr.socket

Some files were not shown because too many files have changed in this diff Show more