- xset
- setxkbmap
- picom
- nitrogen
- xss-lock
- slock
- brightnessctl for controlling the brightness
- playerctl
- scrot for screenshots
- polybar
- dunst
- dunstctl (in Gentoo a part of dunst)
Enable ipc flag on Polybar to use polybar-msg.
doas emerge -qa xset setxkbmap picom nitrogen xss-lock slock polybar dunst mu pass qutebrowser
(Not necesarry, only helpful if you want an email client, and a well intregrated browser for your EXWM)
- mu (install it or delete it’s code from desktop applications section)
- qutebrowser
Only used by a login manager, if no login manager do not tangle
This file is used by yout “login manager” (GDM, LightDM, etc) to display EXWM as a desktop enviroment option when you log in.
IMPORTANT: Make sure you create a symbolic link for this file into /usr/share/xsessions
:
sudo ln -f ~/.emacs.d/exwm/EXWM.desktop /usr/share/xsessions/EXWM.desktop
[Desktop Entry]
NAME=EXWM
Comment=Emacs Window Manager
Exec=sh ~/.emacs.d/exwm/start-exwm.sh
TryExec=sh
Type=Application
DesktopNames=exwm
If not using a login manager you can directly tangle to ~~/.xinitrc~
# Set default cursor
xsetroot -cursor_name left_ptr
# Setting faster typing
xset r rate 200 75
# Converting caps lock to ctrl
setxkbmap -option "ctrl:nocaps"
# Startup picom
picom &
# Resotring previous wallpaper
# Not needed in the launcher script. Launching at exwm startup
# nitrogen --restore &
# Enable screen locking on suspend
xss-lock -- slock &
# Fire it up
# -mm - maximize window
# --debug-init - as init.el is loading if hit any errors drop into the debugger
exec dbus-launch --exit-with-session emacs -mm --debug-init -l ~/.emacs.d/desktop.el
The Xresources
file will be used with xrdb
in start-exwm.sh
to set our screen DPI:
Currently not in use
Xft.dpi: 100 # Set this to your desired DPI! Larger number means bigger text and UI.
(set-frame-parameter (selected-frame) 'fullscreen 'maximized)
(add-to-list 'default-frame-alist '(fullscreen . maximized))
DEPENDENCIES
- brignessctl for controlling the brightness
- playerctl
- scrot for screenshots
- amixer for controlling volume
(use-package desktop-environment
:after exwm
:config
;; Rebinding dekstop-environment-lock-screen form s-l to s-L for integration with windowmove keybinds
(define-key desktop-environment-mode-map (kbd "s-l") nil)
(define-key desktop-environment-mode-map (kbd "s-L") #'desktop-environment-lock-screen)
(desktop-environment-mode)
:custom
;; Brightness increments and decrements
(desktop-environment-brightness-small-increment "1%+")
(desktop-environment-brightness-small-decrement "1%-")
(desktop-environment-brightness-normal-increment "5%+")
(desktop-environment-brightness-normal-decrement "5%-")
;; Volume increments and decrements
(desktop-environment-volume-small-increment "1%+")
(desktop-environment-volume-small-decrement "1%-")
(desktop-environment-volume-normal-increment "5%+")
(desktop-environment-volume-normal-decrement "5%-"))
Using slock and xss-lock. Added to startup file.
DEPENDENCIES
- xss-lock
- slock
Void Linux (xbps):
sudo xbps-install slock xss-lock
Arch Linux:
sudo pacman -S slock xss-lock
Ubuntu (apt):
sudo apt install slock xss-lock
Gentoo:
doas emerge slock xss-lock
Void Linux:
sudo xbps-install polybar
Arch: Use yay to install. or just clone git
yay -S polybar-git
doas emerge -qa polybar
Also install some icon fonts:
Void Linux:
sudo xbps-install font-awesome font-material-design-icons-ttf
; Docs: https://github.com/polybar/polybar
;==========================================================
[settings]
screenchange-reload = true
[global/wm]
margin-top = 0
margin-bottom = 0
[colors]
background = #f0232635
background-alt = #576075
foreground = #A6Accd
foreground-alt = #555
primary = #ffb52a
secondary = #e60053
alert = #bd2c40
underline-1 = #c792ea
[bar/panel]
width = 100%
height = 25
offset-x = 0
offset-y = 0
fixed-center = true
enable-ipc = true
background = ${colors.background}
foreground = ${colors.foreground}
line-size = 2
line-color = #f00
border-size = 0
border-color = #00000000
padding-top = 5
padding-left = 1
padding-right = 1
module-margin = 1
font-0 = "Cantarell:size=14:weight=bold;2"
font-1 = "Font Awesome:size=14;2"
font-2 = "Material Icons:size=18;5"
font-3 = "Fira Mono:size=13;-3"
modules-left = exwm-workspace
modules-right = cpu temperature battery date
tray-position = right
tray-padding = 2
tray-maxsize = 28
cursor-click = pointer
cursor-scroll = ns-resize
[module/exwm-workspace]
type = custom/ipc
hook-0 = emacsclient -e "exwm-workspace-current-index" | sed -e 's/^"//' -e 's/"$//'
initial = 1
format-underline = ${colors.underline-1}
format-padding = 1
[module/cpu]
type = internal/cpu
interval = 1
format = <label> <ramp-coreload>
format-underline = ${colors.underline-1}
click-left = emacsclient -e "(proced)"
label = %percentage:2%%
ramp-coreload-spacing = 0
ramp-coreload-0 = ▁
ramp-coreload-0-foreground = ${colors.foreground-alt}
ramp-coreload-1 = ▂
ramp-coreload-2 = ▃
ramp-coreload-3 = ▄
ramp-coreload-4 = ▅
ramp-coreload-5 = ▆
ramp-coreload-6 = ▇
[module/date]
type = internal/date
interval = 5
date = "%d/%m/%y"
date-alt = "%a %d/%m/%y"
time = %H:%M
time-alt = %H:%M:%S
format-prefix-foreground = ${colors.foreground-alt}
format-underline = ${colors.underline-1}
label = %date% %time%
[module/battery]
type = internal/battery
battery = BAT0
adapter = ADP1
full-at = 98
time-format = %-l:%M
label-charging = %percentage%% / %time%
format-charging = <animation-charging> <label-charging>
format-charging-underline = ${colors.underline-1}
label-discharging = %percentage%% / %time%
format-discharging = <ramp-capacity> <label-discharging>
format-discharging-underline = ${self.format-charging-underline}
format-full = <ramp-capacity> <label-full>
format-full-underline = ${self.format-charging-underline}
ramp-capacity-0 =
ramp-capacity-1 =
ramp-capacity-2 =
ramp-capacity-3 =
ramp-capacity-4 =
animation-charging-0 =
animation-charging-1 =
animation-charging-2 =
animation-charging-3 =
animation-charging-4 =
animation-charging-framerate = 750
[module/temperature]
type = internal/temperature
; Thermal zone to use
; To list all the zone types, run
; $ for i in /sys/class/thermal/thermal_zone*; do echo "$i: $(<$i/type)"; cat $i/temp; done
thermal-zone = 1
warn-temperature = 60
format = <label>
format-underline = ${colors.underline-1}
format-warn = <label-warn>
format-warn-underline = ${self.format-underline}
label = %temperature-c%
label-warn = %temperature-c%!
label-warn-foreground = ${colors.secondary}
(defvar linas/polybar-process nil
"Holds the process of the running Polybar instance, if any")
(defun linas/kill-panel ()
(interactive)
(when linas/polybar-process
(ignore-errors
(kill-process linas/polybar-process))))
(defun linas/start-panel ()
(interactive)
(linas/kill-panel)
(setq linas/polybar-process (start-process-shell-command "polybar" nil "polybar panel")))
(defun linas/send-polybar-hook (module-name hook-index)
(start-process-shell-command "polybar-msg" nil (format "polybar-msg hook %s %s" module-name hook-index)))
(defun linas/send-polybar-exwm-workspace ()
(linas/send-polybar-hook "exwm-workspace" 1))
;; Update panel indicator when workspace changes
(add-hook 'exwm-workspace-switch-hook #'linas/send-polybar-exwm-workspace)
Dependencies
- Dunst
- dunstctl (comes with dunst from version 1.5.0)
Void Linux:
sudo xbps-install dunst
[global]
### Display ###
monitor = 0
# The geometry of the window:
# [{width}]x{height}[+/-{x}+/-{y}]
geometry = "300x10-10+30"
# Show how many messages are currently hidden (because of geometry).
indicate_hidden = yes
# Shrink window if it's smaller than the width. Will be ignored if
# width is 0.
shrink = no
# The transparency of the window. Range: [0; 100].
transparency = 10
# The height of the entire notification. If the height is smaller
# than the font height and padding combined, it will be raised
# to the font height and padding.
notification_height = 0
# Draw a line of "separator_height" pixel height between two
# notifications.
# Set to 0 to disable.
separator_height = 1
separator_color = frame
# Padding between text and separator.
padding = 8
# Horizontal padding.
horizontal_padding = 8
# Defines width in pixels of frame around the notification window.
# Set to 0 to disable.
frame_width = 2
# Defines color of the frame around the notification window.
frame_color = "#89AAEB"
# Sort messages by urgency.
sort = yes
# Don't remove messages, if the user is idle (no mouse or keyboard input)
# for longer than idle_threshold seconds.
idle_threshold = 30
### Text ###
font = Cantarell 12
# The spacing between lines. If the height is smaller than the
# font height, it will get raised to the font height.
line_height = 0
markup = full
# The format of the message. Possible variables are:
# %a appname
# %s summary
# %b body
# %i iconname (including its path)
# %I iconname (without its path)
# %p progress value if set ([ 0%] to [100%]) or nothing
# %n progress value if set without any extra characters
# %% Literal %
# Markup is allowed
format = "<b>%s</b>\n%b"
# Alignment of message text.
# Possible values are "left", "center" and "right".
alignment = left
# Show age of message if message is older than show_age_threshold
# seconds.
# Set to -1 to disable.
show_age_threshold = 15
# Split notifications into multiple lines if they don't fit into
# geometry.
word_wrap = yes
# When word_wrap is set to no, specify where to make an ellipsis in long lines.
# Possible values are "start", "middle" and "end".
ellipsize = middle
# Ignore newlines '\n' in notifications.
ignore_newline = no
# Stack together notifications with the same content
stack_duplicates = true
# Hide the count of stacked notifications with the same content
hide_duplicate_count = false
# Display indicators for URLs (U) and actions (A).
show_indicators = yes
### Icons ###
# Align icons left/right/off
icon_position = left
# Scale larger icons down to this size, set to 0 to disable
max_icon_size = 88
# Paths to default icons. Use : to add multiple paths
icon_path = /usr/share/icons/Adwaita/48x48/emblems:/usr/share/icons/Adwaita/48x48/status
### History ###
# Should a notification popped up from history be sticky or timeout
# as if it would normally do.
sticky_history = no
# Maximum amount of notifications kept in history
history_length = 20
### Misc/Advanced ###
# Browser for opening urls in context menu.
browser = qutebrowser
# Always run rule-defined scripts, even if the notification is suppressed
always_run_script = true
# Define the title of the windows spawned by dunst
title = Dunst
# Define the class of the windows spawned by dunst
class = Dunst
startup_notification = false
verbosity = mesg
# Define the corner radius of the notification window
# in pixel size. If the radius is 0, you have no rounded
# corners.
# The radius will be automatically lowered if it exceeds half of the
# notification height to avoid clipping text and/or icons.
corner_radius = 4
mouse_left_click = close_current
mouse_middle_click = do_action
mouse_right_click = close_all
# Experimental features that may or may not work correctly. Do not expect them
# to have a consistent behaviour across releases.
[experimental]
# Calculate the dpi to use on a per-monitor basis.
# If this setting is enabled the Xft.dpi value will be ignored and instead
# dunst will attempt to calculate an appropriate dpi value for each monitor
# using the resolution and physical size. This might be useful in setups
# where there are multiple screens with very different dpi values.
per_monitor_dpi = false
[shortcuts]
# Shortcuts are specified as [modifier+][modifier+]...key
# Available modifiers are "ctrl", "mod1" (the alt-key), "mod2",
# "mod3" and "mod4" (windows-key).
# Xev might be helpful to find names for keys.
# Close notification.
#close = ctrl+space
# Close all notifications.
#close_all = ctrl+shift+space
# Redisplay last message(s).
# On the US keyboard layout "grave" is normally above TAB and left
# of "1". Make sure this key actually exists on your keyboard layout,
# e.g. check output of 'xmodmap -pke'
history = ctrl+grave
# Context menu.
context = ctrl+shift+period
[urgency_low]
# IMPORTANT: colors have to be defined in quotation marks.
# Otherwise the "#" and following would be interpreted as a comment.
background = "#222222"
foreground = "#888888"
timeout = 10
# Icon for notifications with low urgency, uncomment to enable
#icon = /path/to/icon
[urgency_normal]
background = "#1c1f26"
foreground = "#ffffff"
timeout = 10
# Icon for notifications with normal urgency, uncomment to enable
#icon = /path/to/icon
[urgency_critical]
background = "#900000"
foreground = "#ffffff"
frame_color = "#ff0000"
timeout = 0
# Icon for notifications with critical urgency, uncomment to enable
#icon = /path/to/icon
(defun linas/dunstctl (command)
(start-process-shell-command "dunstctl" nil (concat "dunstctl " command)))
(defun linas/desktop-notifications-enable ()
(interactive)
(linas/dunstctl "set-paused false"))
(defun linas/desktop-notifications-disable ()
(interactive)
(linas/dunstctl "set-paused true"))
(defun linas/desktop-notifications-toggle ()
(interactive)
(linas/dunstctl "set-paused toggle"))
Function for switching keyboard layout
(defun linas/switch-keyboard-layout (to_layout)
"Function to switch between keyboard layouts"
(interactive
(list (completing-read "MSwitch keybaord layout to: "
'(("us") ("no") ("lt")) nil t)))
(start-process-shell-command "setxkbmap" nil (format "setxkbmap %s" to_layout)))
;; later use for polybar to display keyboard layout
(defun linas/get-keyboard-layout ()
(interactive))
(use-package ncla
:straight '(NCLA :host github :repo "LinasVidziunas/NCLA"))
(defun linas/run-in-background (command)
(let ((command-parts (split-string command "[ ]+")))
(apply #'call-process `(,(car command-parts) nil 0 nil ,@(cdr command-parts)))))
(defun linas/exwm-init-hook ()
;; Make workspace 1 to be the one where we land at startup
;; (exwm-workspace-switch-create 1)
;; Open eshell by defaul
;; (eshell)
;;Launch apps that will run in the background
(linas/run-in-background "nm-applet")
(linas/run-in-background "pasystray")
(linas/run-in-background "blueman-applet")
(linas/run-in-background "dunst"))
;; Start the Polybar panel
(linas/start-panel)
(defun linas/exwm-update-class ()
(exwm-workspace-rename-buffer exwm-class-name))
(defun linas/exwm-update-title-firefox-remove-double-name ()
(if (string-match-p " — Mozilla Firefox\\'" exwm-title)
(substring exwm-title 0 (string-match-p " — Mozilla Firefox\\'" exwm-title))
exwm-title))
(defun linas/exwm-update-title-qutebrowser-remove-double-name ()
(if (string-match-p " - qutebrowser\\'" exwm-title)
(substring exwm-title 0 (string-match-p " - qutebrowser\\'" exwm-title))
exwm-title))
;; Not finished, maninly cuz im retarded
;; (defun linas/exwm-update-title-firefox ()
;; (interactive)
;; (message (length (linas/exwm-update-title-firefox-remove-double-name)))
;; (if (> (length linas/exwm-update-title-firefox-remove-double-name) 60)
;; (concat (substring exwm-title 0 60) "...")
;; (linas/exwm-update-title-firefox-remove-double-name)))
(defun linas/exwm-update-title ()
(pcase exwm-class-name
("Firefox"
(exwm-workspace-rename-buffer
(format "Firefox: %s" (linas/exwm-update-title-firefox-remove-double-name))))
("qutebrowser"
(exwm-workspace-rename-buffer
(format "Qutebrowser: %s" (linas/exwm-update-title-qutebrowser-remove-double-name))))))
(defun linas/configure-window-by-class ()
(interactive)
;; (message "Window '%s' appeared!" exwm-class-name)
(pcase exwm-class-name
("mpv" (exwm-floating-toggle-floating)
(exwm-layout-toggle-mode-line))
("Firefox" (exwm-workspace-move-window 0))))
(defun linas/position-window ()
(let* ((pos (frame-position)
(pos-x (car pos))
(pos-y (cdr pos)))
(exwm-floating-move (- pos-x) (- pos-y)))))
(defun linas/set-wallpaper ()
(interactive)
(start-process-shell-command
"nitrogen" nil "nitrogen --restore"))
(use-package exwm
:config
;; Set the default number of workspaces
(setq exwm-workspace-number 10)
;; Automatically move EXWM buffer to current workspace when slected
;; exwm-switch-to-buffer
;; (setq exwm-layout-show-all-buffers t)
;; Display all EXWM buffers in every workspace buffer list
;; (setq exwm-workspace-show-all-buffers t)
;; When window "class" updates, use it to set the buffer name
(add-hook 'exwm-update-class-hook #'linas/exwm-update-class)
;; When window title updates, use it to set the buffer name
(add-hook 'exwm-update-title-hook #'linas/exwm-update-title)
;; When EXWM starts up, do some extra configuration
(add-hook 'exwm-init-hook #'linas/exwm-init-hook)
;; Set the screen resolution
(require 'exwm-randr)
(exwm-randr-enable)
(start-process-shell-command "xrandr" nil "xrandr --output eDP1 --primary --mode 1920x1080 --pos 0x0 --rotate normal")
(linas/set-wallpaper)
;; Load the system tray before exwm-init
;; Commented out because using polybar
;; (require 'exwm-systemtray)
;; (setq exwm-systemtray-height 20)
;; (exwm-systemtray-enable)
;; These keys should always pass through to Emacs
(setq exwm-input-prefix-keys
'(?\C-x
?\C-u
?\C-h
?\M-x
?\M-`
?\M-&
?\M-:
?\C-\M-j ;; Buffer list
?\C-\ )) ;; Ctrl+Space
;; Ctrl+Q will enable the next key to be sent directly
(define-key exwm-mode-map [?\C-q] 'exwm-input-send-next-key)
;; Set up global key bindings. These always work, no matter the input state!
;; Keep in mind that changing this list after EXWM initializes has no effect.
(setq exwm-input-global-keys
`(
;; Reset to line-mode (C-c C-k switches to char-mode via exwm-input-release-keyboard)
([?\s-r] . exwm-reset)
;; Move between windows
([?\s-h] . windmove-left)
([?\s-l] . windmove-right)
([?\s-k] . windmove-up)
([?\s-j] . windmove-down)
;; Launch applications via shell command
([?\s-&] . (lambda (command)
(interactive (list (read-shell-command "$ ")))
(start-process-shell-command command nil command)))
;; Switch workspace
;;([?\s-w] . exwm-workspace-switch)
;; 's-N': Switch to certain workspace with Super (Win) plus a number key (0 - 9)
,@(mapcar (lambda (i)
`(,(kbd (format "s-%d" i)) .
(lambda ()
(interactive)
(exwm-workspace-switch-create ,i))))
(number-sequence 0 9))))
(exwm-input-set-key (kbd "s-SPC") 'ncla)
(exwm-input-set-key (kbd "s-f") 'exwm-layout-toggle-fullscreen)
(exwm-input-set-key (kbd "s-n") (lambda () (interactive) (linas/dunstctl "history-pop")))
(exwm-input-set-key (kbd "s-N") (lambda () (interactive) (linas/dunstctl "close-all")))
(exwm-enable))
Living nightmare currently
For pass to work with firefox on void.
I installed browserpass
on void and also the browserpass
firefox extenion.
Also installed pinentry-emacs
The auth-source
library looks for passwords in a set of sources configured by the variable auth-sources.
By default it looks at:
~/.authinfo.gpg
~/.authinfo
~/.netrc
Passwords stored in a file named ~/.authinfo
in this format, one per line:
machine facebook.com login zuck password w0rldd0m1n4ti0n
machine mailprovider.com login mailuser password b4dp4ssw0rd port 433
machine mailprovider.com login mailuser password worsepassword
The auth-source-search
function can read this file and search for entries based on any of the details they contain:
(auth-source-search :host "facebook.com")
(auth-source-search :host "mailprovider.com" :user "mailuser")
(auth-source-search :host "mailprovider.com" :user "mailuser" :port 433)
The benefit of using .authinfo
is that it’s a file you have control over and can sync between systems (once encrypted!) More easily portable than using one of the desktop environment keyrings.
However, this file is plaintext by default, which is unsafe from a security standpoint!
Emacs uses GnuPG via the epa
library to automatically encrypt and decrypt any files that end with .gpg
, so we can create a file named .authinfo.gpg
with the same contents to have them be encrypted on save.
Emacs Manual: Easy PGP Assistant (epa
)
But first, we need to generate an encryption key! The following command (in GPG 2.2 and above) will walk you through the process of creating a new key:
gpg --full-generate-key
We need to answer some questions it asks us:
- What kind of key do you want? (1) RSA and RSA (default)
- What keysize do you want? 4096
- How long should the key be valid? 0 (Key does not expire)
- Enter your name
- Enter your e-mail address
- Enter a comment for the key (not necessary, but can be used to identify it)
- If everything looks good, press O for “Okay”
- You will now be prompted for a passphrase. This is like a password for your encryption key, it should be secure and memorable!
- After entering the password, it will generate the new key. Move the mouse around or press keyboard keys to help generate entropy.
You should now have a new key that will show up when you run the following command:
gpg --list-keys
We can test this out by editing the new ~/.authinfo.gpg
file and then paste the contents from the original ~/.authinfo
file we created.
Once you save the ~/.authinfo.gpg
file, a new Emacs window will appear and you will be prompted for which key to use to encrypt the file:
Select recipients for encryption.
If no one is selected, symmetric encryption will be performed.
- ‘m’ to mark a key on the line
- ‘u’ to unmark a key on the line
[Cancel][OK]
If you only have one encryption key, this is all that will appear. You merely need to move your keyboard cursor on top of the string [OK]
and press enter. You will be prompted for your passphrase to unlock the key and the file will be encrypted once you save it successfully.
If you have more than one encryption key, they will be listed below the prompt:
Select recipients for encryption.
If no one is selected, symmetric encryption will be performed.
- ‘m’ to mark a key on the line
- ‘u’ to unmark a key on the line
[Cancel][OK]
u FF0E73B64BBEB63F System Crafters (Password Encryption Key) <[email protected]>
u C0495F71F74DC5E9 David Wilson <[email protected]>
You will need to move your keyboard cursor to the line with the key you would like to use and press the letter m
to mark the key, then move the cursor to [OK]
and press Enter. You will be prompted for your passphrase to unlock the key and the file will be encrypted once you save it successfully.
You can verify that the file is encrypted by trying to read it at the shell:
cat ~/.authinfo.gpg
You can also double-check that the passwords are accessible to auth-source-search
:
(auth-source-search :host "facebook.com" :user "96801405")
(auth-source-search :host "mailprovider.com" :user "mailuser")
(auth-source-search :host "mailprovider.com" :user "mailuser" :port 433)
The gpg-agent
manages access to your PGP keys and assists with encryption and decryption of files. It can also cache your passphrase so that you don’t get prompted for it every time you try to encrypt or decrypt a file.
Emacs’ epa
library may be able to automatically start it for you when you try to encrypt or decrypt a file. If it doesn’t, you may need to start it yourself!
We need to make sure the gpg-agent
is running:
# Check if gpg-agent is already running
pgrep gpg-agent
# If it's not running, you can start it up with this command:
gpg-connect-agent /bye
In Ubuntu 20.04, it seems to be started as a user service. If it isn’t running by default in your system, you may need to add gpg-connect-agent /bye
as a startup command in your desktop environment or however you start Xorg sessions.
If you have Emacs running as a daemon or in server mode (see my video on that) you can use emacsclient
to access your passwords from other programs (like mbsync
, etc).
First we’ll create a helper function to add to our configuration to make this a little easier to call:
(defun linas/lookup-password (&rest keys)
(interactive)
(let ((result (apply #'auth-source-search keys)))
(if result
(funcall (plist-get (car result) :secret))
nil)))
Now you can invoke emacsclient
in the shell to run this function and process the result:
emacsclient -e "(linas/lookup-password :host \"gmail.com\" :user \"[email protected]\")" | cut -d '"' -f2
Any program that can call an external shell application can now use this line to request the unencrypted password! When the password is requested, you will be prompted for your passphrase if it has been a while since the last time you were asked.
For example, in the mbsync
config from the Emacs Mail series:
PassCmd "emacsclient -e \"(linas/lookup-password :host \\\"gmail.com\\\" :user \\\"systemcrafters.test\\\")\" | cut -d '\"' -f2"
Dependencies
- pinentry-emacs
use-agent
pinentry-mode loopback
# this means clients like Emacs can get the password in their own way and push to gpg
# for this to work with Emacs, set epa-pinentry-mode to 'loopback in Emacs
# allow-loopback-pinentry
allow-emacs-pinentry
Then tell gpg-agent to load this configuration with gpgconf
in shell:
gpgconf --reload gpg-agent
(setq epg-pinentry-mode 'loopback)
Today we’ll sync a Gmail account since they’re pretty common. In another episode we’ll show a more traditional IMAP account too.
We’ll use a program called isync
(in practice it’s mbsync
!) to sync our mail. You can also use a program called offlineimap
, it’s a bit slower but works on Windows too.
Void Linux install:
sudo xbps-install isync
Set up an initial configuration at ~/.mbsyncrc:
IMAPAccount gmail
Host imap.gmail.com
User [email protected]
PassCmd "emacsclient -e \"(linas/lookup-password :host \\\"gmail.com\\\" :user \\\"[email protected]\\\")\" | cut -d '\"' -f2"
SSLType IMAPS
CertificateFile /etc/ssl/certs/ca-certificates.crt
IMAPStore gmail-remote
Account gmail
MaildirStore gmail-local
Subfolders Verbatim
Path ~/Mail/[email protected]/
Inbox ~/Mail/[email protected]/Inbox
Channel gmail
Master :gmail-remote:
Slave :gmail-local:
Patterns * ![Gmail]* "[Gmail]/Sent Mail" "[Gmail]/Starred" "[Gmail]/All Mail" "[Gmail]/Trash"
Create Both
SyncState *
IMAPAccount school
Host outlook.office365.com
Port 993
User [email protected]
PassCmd "emacsclient -e \"(linas/lookup-password :host \\\"office365.com\\\" :user \\\"[email protected]\\\")\" | cut -d '\"' -f2"
UseIMAPS yes
CertificateFile /etc/ssl/certs/ca-certificates.crt
IMAPStore school-remote
Account school
MaildirStore school-local
Subfolders Verbatim
Path ~/Mail/school/
Inbox ~/Mail/school/Inbox
Channel school
Master :school-remote:
Slave :school-local:
# Include everything
Patterns "INBOX" "*"
# Automatically create missing mailboxes, both locally and on the server
Create Both
Sync All
# Save the synchronization state files in the relevant directory
SyncState *
Install mu4e
package:
Void Linux:
sudo xbps-install mu4e
Run the initial index, providing your e-mail address so it knows how to identify you:
mu index --maildir=~/Mail [email protected]
(use-package mu4e
:straight nil
:load-path "/usr/share/emacs/site-lisp/mu4e/"
:defer 10 ; Wait until 10 seconds after startup
:config
;; Pull in org helpers
(require 'mu4e-org)
;; This is set to 't' to avoid mail syncing issues when using mbsync
(setq mu4e-change-filenames-when-moving t)
;; Refresh mail using isync every 2 minutes
(setq mu4e-update-interval (* 2 60))
(setq mu4e-get-mail-command "mbsync -a")
(setq mu4e-maildir "~/Mail")
(setq mu4e-contexts
(list
;; Main personal account
(make-mu4e-context
:name "[email protected]"
:match-func
(lambda (msg)
(when msg
(string-prefix-p "/[email protected]" (mu4e-message-field msg :maildir))))
:vars '((user-mail-address . "[email protected]")
(user-full-name . "Linas Vidziunas")
(mu4e-compose-signature . "Linas Vidziunas")
(smtpmail-smtp-server . "smtp.gmail.com")
(smtpmail-smtp-service . 465)
(smtpmail-stream-type . ssl)
(mu4e-drafts-folder . "/[email protected]/[Gmail]/Drafts")
(mu4e-sent-folder . "/[email protected]/[Gmail]/Sent Mail")
(mu4e-refile-folder . "/[email protected]/[Gmail]/All Mail")
(mu4e-trash-folder . "/[email protected]/[Gmail]/Trash")))
;; School account
(make-mu4e-context
:name "school"
:match-func
(lambda (msg)
(when msg
(string-prefix-p "/school" (mu4e-message-field msg :maildir))))
:vars '((user-mail-address . "[email protected]")
(user-full-name . "Linas Vidziunas")
(mu4e-compose-signature . "Linas Vidzinas (254664)")
(smtpmail-smtp-server . "smtp.office365.com")
(smtpmail-smtp-service . 587)
(smtpmail-stream-type . starttls)
(mu4e-drafts-folder . "/school/Drafts")
(mu4e-sent-folder . "/school/Sent Mail")
(mu4e-refile-folder . "/school/All Mail")
(mu4e-trash-folder . "/school/Trash")))))
;; Email Capture Templates
(setq org-capture-templates
`(("m" "Email Workflow")
("mf" "Follow Up" entry (file+olp "~/org/Mail.org" "Follow Up")
"* TODO Follow up with %:fromname on %a\n SCHEDULED:%t\n DEADLINE:%(org-insert-time-stamp (org-read-date nil t \"+2d\"))\n\n <%:fromaddress> on %:date-timestamp\n\n\t%a\n\n\t%i"
:immediate-finish t)
("mr" "Read Later" entry (file+olp "~/org/Mail.org" "Read Later")
"* TODO Read %a\n SCHEDULED:%t\n DEADLINE:%(org-insert-time-stamp (org-read-date nil t \"+2d\"))\n\n <%:fromaddress> on %:date-timestamp\n\n\t%a\n\n\t%i"
:immediate-finish t)))
(defun linas/capture-mail-follow-up (msg)
(interactive)
(call-interactively 'org-store-link)
(org-capture nil "mf"))
(defun linas/capture-mail-read-later (msg)
(interactive)
(call-interactively 'org-store-link)
(org-capture nil "mf"))
(defun linas/store-link-to-mu4e-query ()
(interactive)
(let ((mu4e-org-link-query-in-headers-mode t))
(call-interactively 'org-store-link)))
;; Add custom actions for our capture templates
(add-to-list 'mu4e-headers-actions
'("follow up" . linas/capture-mail-follow-up) t)
(add-to-list 'mu4e-view-actions
'("follow up" . linas/capture-mail-follow-up) t)
(add-to-list 'mu4e-headers-actions
'("read later" . linas/capture-mail-read-later) t)
(add-to-list 'mu4e-view-actions
'("read later" . linas/capture-mail-read-later) t)
(setq mu4e-maildir-shortcuts
'((:maildir "/[email protected]/Inbox" :key ?i)
(:maildir "/[email protected]/[Gmail]/Sent Mail" :key ?s)
(:maildir "/[email protected]/[Gmail]/Trash" :key ?t)
(:maildir "/[email protected]/[Gmail]/Drafts" :key ?d)
(:maildir "/[email protected]/[Gmail]/All Mail" :key ?a)))
;; Annoyingly the first mail has to be sent synchrounously
(setq send-mail-function 'smtpmail-send-it)
(setq message-send-mail-function 'smtpmail-send-it)
;; My attemt at async mail
;; (setq linas/smtp-to-async-smtp-mails-sent 0)
;; (defun linas/smtp-to-async-smtp ()
;; (message "Message sent synchronously")
;; ;; Why 2? how the fuck im I supposed to know
;; (when (> linas/smtp-to-async-smtp-mails-sent 2)
;; (require 'smtpmail-async)
;; (message "Message sent asynchronously")
;; (setq send-mail-function 'async-smtpmail-send-it)
;; (setq message-send-mail-function 'async-smtpmail-send-it))
;; (setq linas/smtp-to-async-smtp-mails-sent (+ linas/smtp-to-async-smtp-mails-sent 1)))
;; (add-hook 'message-sent-hook #'linas/smtp-to-async-smtp)
;; When enters main view picks the first context (first email)
(setq mu4e-context-policy 'pick-first)
;; Make sure plain text mails flow correctly for recipients
(setq mu4e-compose-format-flowed t)
;; Defaults
;; (setq mu4e-date-format-long "%c")
;; (setq mu4e-view-date-format "%c")
;; (setq mu4e-headers-long-date-format "%c")
;; (setq mu4e-headers-date-format "%x")
(setq mu4e-headers-date-format "%d/%m/%y")
(mu4e t))
(use-package mu4e-column-faces
:after mu4e
:config (mu4e-column-faces-mode))
(use-package mu4e-alert
:after mu4e
:custom
;; Disable double notifications per email. annoying
(mu4e-alert-email-notification-types '(subjects))
:config
(mu4e-alert-set-default-style 'libnotify)
(mu4e-alert-enable-notifications))
;; mu4e-alert fix
;; Annoying but looks like dev is dead
(defun mu4e-alert--get-mu4e-frame ()
"Try getting a frame containing a mu4e buffer."
(car (delq nil (mapcar (lambda (buffer)
(when (and buffer
(get-buffer-window buffer t))
(window-frame (get-buffer-window buffer t))))
(list mu4e-main-buffer-name)))))
(defun mu4e-alert-filter-repeated-mails (mails)
"Filters the MAILS that have been seen already."
(cl-remove-if (lambda (mail)
(prog1 (and (not mu4e-alert-notify-repeated-mails)
(ht-get mu4e-alert-repeated-mails
(plist-get mail :message-id)))
(ht-set! mu4e-alert-repeated-mails
(plist-get mail :message-id)
t)
))
mails))
Important Use M-x org-mime-htmlize
to htmlize org formatted text. Use M-x org-mime-edit-mail-in-org-mode
to compose in org mode.
(use-package org-mime
:after mu4e
:custom
(org-mime-export-options '(:section-numbers nil
:with-author nil
:with-toc nil))
:config
(add-hook 'message-send-hook 'org-mime-confirm-when-no-multipart))
# Autogenerated config.py
#
# NOTE: config.py is intended for advanced users who are comfortable
# with manually migrating the config file on qutebrowser upgrades. If
# you prefer, you can also configure qutebrowser using the
# :set/:bind/:config-* commands without having to write a config.py
# file.
#
# Documentation:
# qute://help/configuring.html
# qute://help/settings.html
# Change the argument to True to still load settings configured via autoconfig.yml
config.load_autoconfig(False)
# Which cookies to accept. With QtWebEngine, this setting also controls
# other features with tracking capabilities similar to those of cookies;
# including IndexedDB, DOM storage, filesystem API, service workers, and
# AppCache. Note that with QtWebKit, only `all` and `never` are
# supported as per-domain values. Setting `no-3rdparty` or `no-
# unknown-3rdparty` per-domain on QtWebKit will have the same effect as
# `all`. If this setting is used with URL patterns, the pattern gets
# applied to the origin/first party URL of the page making the request,
# not the request URL. With QtWebEngine 5.15.0+, paths will be stripped
# from URLs, so URL patterns using paths will not match. With
# QtWebEngine 5.15.2+, subdomains are additionally stripped as well, so
# you will typically need to set this setting for `example.com` when the
# cookie is set on `somesubdomain.example.com` for it to work properly.
# To debug issues with this setting, start qutebrowser with `--debug
# --logfilter network --debug-flag log-cookies` which will show all
# cookies being set.
# Type: String
# Valid values:
# - all: Accept all cookies.
# - no-3rdparty: Accept cookies from the same origin only. This is known to break some sites, such as GMail.
# - no-unknown-3rdparty: Accept cookies from the same origin only, unless a cookie is already set for the domain. On QtWebEngine, this is the same as no-3rdparty.
# - never: Don't accept cookies at all.
config.set('content.cookies.accept', 'all', 'chrome-devtools://*')
# Which cookies to accept. With QtWebEngine, this setting also controls
# other features with tracking capabilities similar to those of cookies;
# including IndexedDB, DOM storage, filesystem API, service workers, and
# AppCache. Note that with QtWebKit, only `all` and `never` are
# supported as per-domain values. Setting `no-3rdparty` or `no-
# unknown-3rdparty` per-domain on QtWebKit will have the same effect as
# `all`. If this setting is used with URL patterns, the pattern gets
# applied to the origin/first party URL of the page making the request,
# not the request URL. With QtWebEngine 5.15.0+, paths will be stripped
# from URLs, so URL patterns using paths will not match. With
# QtWebEngine 5.15.2+, subdomains are additionally stripped as well, so
# you will typically need to set this setting for `example.com` when the
# cookie is set on `somesubdomain.example.com` for it to work properly.
# To debug issues with this setting, start qutebrowser with `--debug
# --logfilter network --debug-flag log-cookies` which will show all
# cookies being set.
# Type: String
# Valid values:
# - all: Accept all cookies.
# - no-3rdparty: Accept cookies from the same origin only. This is known to break some sites, such as GMail.
# - no-unknown-3rdparty: Accept cookies from the same origin only, unless a cookie is already set for the domain. On QtWebEngine, this is the same as no-3rdparty.
# - never: Don't accept cookies at all.
config.set('content.cookies.accept', 'all', 'devtools://*')
# Value to send in the `Accept-Language` header. Note that the value
# read from JavaScript is always the global value.
# Type: String
config.set('content.headers.accept_language', '', 'https://matchmaker.krunker.io/*')
# User agent to send. The following placeholders are defined: *
# `{os_info}`: Something like "X11; Linux x86_64". * `{webkit_version}`:
# The underlying WebKit version (set to a fixed value with
# QtWebEngine). * `{qt_key}`: "Qt" for QtWebKit, "QtWebEngine" for
# QtWebEngine. * `{qt_version}`: The underlying Qt version. *
# `{upstream_browser_key}`: "Version" for QtWebKit, "Chrome" for
# QtWebEngine. * `{upstream_browser_version}`: The corresponding
# Safari/Chrome version. * `{qutebrowser_version}`: The currently
# running qutebrowser version. The default value is equal to the
# unchanged user agent of QtWebKit/QtWebEngine. Note that the value
# read from JavaScript is always the global value. With QtWebEngine
# between 5.12 and 5.14 (inclusive), changing the value exposed to
# JavaScript requires a restart.
# Type: FormatString
config.set('content.headers.user_agent', 'Mozilla/5.0 ({os_info}) AppleWebKit/{webkit_version} (KHTML, like Gecko) {upstream_browser_key}/{upstream_browser_version} Safari/{webkit_version}', 'https://web.whatsapp.com/')
# User agent to send. The following placeholders are defined: *
# `{os_info}`: Something like "X11; Linux x86_64". * `{webkit_version}`:
# The underlying WebKit version (set to a fixed value with
# QtWebEngine). * `{qt_key}`: "Qt" for QtWebKit, "QtWebEngine" for
# QtWebEngine. * `{qt_version}`: The underlying Qt version. *
# `{upstream_browser_key}`: "Version" for QtWebKit, "Chrome" for
# QtWebEngine. * `{upstream_browser_version}`: The corresponding
# Safari/Chrome version. * `{qutebrowser_version}`: The currently
# running qutebrowser version. The default value is equal to the
# unchanged user agent of QtWebKit/QtWebEngine. Note that the value
# read from JavaScript is always the global value. With QtWebEngine
# between 5.12 and 5.14 (inclusive), changing the value exposed to
# JavaScript requires a restart.
# Type: FormatString
config.set('content.headers.user_agent', 'Mozilla/5.0 ({os_info}; rv:90.0) Gecko/20100101 Firefox/90.0', 'https://accounts.google.com/*')
# User agent to send. The following placeholders are defined: *
# `{os_info}`: Something like "X11; Linux x86_64". * `{webkit_version}`:
# The underlying WebKit version (set to a fixed value with
# QtWebEngine). * `{qt_key}`: "Qt" for QtWebKit, "QtWebEngine" for
# QtWebEngine. * `{qt_version}`: The underlying Qt version. *
# `{upstream_browser_key}`: "Version" for QtWebKit, "Chrome" for
# QtWebEngine. * `{upstream_browser_version}`: The corresponding
# Safari/Chrome version. * `{qutebrowser_version}`: The currently
# running qutebrowser version. The default value is equal to the
# unchanged user agent of QtWebKit/QtWebEngine. Note that the value
# read from JavaScript is always the global value. With QtWebEngine
# between 5.12 and 5.14 (inclusive), changing the value exposed to
# JavaScript requires a restart.
# Type: FormatString
config.set('content.headers.user_agent', 'Mozilla/5.0 ({os_info}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99 Safari/537.36', 'https://*.slack.com/*')
# Load images automatically in web pages.
# Type: Bool
config.set('content.images', True, 'chrome-devtools://*')
# Load images automatically in web pages.
# Type: Bool
config.set('content.images', True, 'devtools://*')
# Enable JavaScript.
# Type: Bool
config.set('content.javascript.enabled', True, 'chrome-devtools://*')
# Enable JavaScript.
# Type: Bool
config.set('content.javascript.enabled', True, 'devtools://*')
# Enable JavaScript.
# Type: Bool
config.set('content.javascript.enabled', True, 'chrome://*/*')
# Enable JavaScript.
# Type: Bool
config.set('content.javascript.enabled', True, 'qute://*/*')
# Tabs are windows for EXWM, switch tabs with counsel switch buffer
config.set('tabs.tabs_are_windows', True)
# And never show tabs
config.set('tabs.show', "never")
To get qute pass to work you need a python3 module called tldextract
On Void Linux:
sudo xbps-install python3-tldextract
After installation test it by running in qutebrowser
:spawn --userscript qute-pass
You may also need to change the permissions in for ‘/usr/lib/python3.9/site-packages/tldextract/tld_set’, as I had to on Linux Void Giving usergroup permission to write and read:
sudo chmod g+wr /usr/lib/python3.9/site-packages/tldextract/
(defun linas/proced-settings ()
(setq proced-format (list 'pid 'tree 'pcpu 'pmem (list 'args 'comm)))
(proced-toggle-tree t)
(proced-toggle-auto-update t))
(use-package proced
:straight nil
:config
(setq proced-auto-update-interval 1)
(add-hook 'proced-mode-hook 'linas/proced-settings))