Nix-Darwin-Config + Doom Emacs Configuration
Nix-powered declarative macOS configuration
Let us change our traditional attitude to the construction of programs: Instead of imagining that our main task is to instruct a computer what to do, let us concentrate rather on explaining to human beings what we want a computer to do. — Donald Knuth
1. Introduction
Once upon a time I was a wee little lad playing around with vim. After that, my “ricing” addiction grew, and soon it turned into a dotfiles repo. Since I moved machines often, I wanted a simple way to install all dependencies for my system. What started off as a simple install.sh script turned into a dotfiles repo managed via YADM. However this raised a few issues:
- It was slow and clunky. Apps like Discord and Firefox started to clutter up my ~/.config directory, and my .gitignore kept growing. With nix, my config is stored in one folder, and symlinked into place
- Applications were all configured using different languages. With home-manager for the most part I can stick to using nix,
- Building apps was a pain, and switching laptops was getting annoying.
1.1. Note On Installing
If you like the look of this, that’s marvellous, and I’m really happy that I’ve made something which you may find interesting, however:
This config is insidious. Copying the whole thing blindly can easily lead to undesired effects. I recommend copying chunks instead.
Oh, did I mention that I started this config when I didn’t know any nix or lisp, and this whole thing is a hack job? If you can suggest any improvements, please do so, no matter how much criticism you include I’ll appreciate it :)
1.2. Why Nix?
Nix consists of two parts: a package manager and a language. The language is a rather simple lazy (almost) pure functional language with dynamic typing that specializes in building packages. The package manager, on the other hand, is interesting and pretty unique. It all starts with one idea.
Nix stems from the idea that FHS is fundamentally incompatible with reproducibility. Every time you see a path like /bin/python or /lib/libudev.so, there are a lot of things that you don’t know about the file that’s located there.
What’s the version of the package it came from? What are the libraries it uses? What configure flags were enabled during the build? Answers to these questions can (and most likely will) change the behaviour of an application that uses those files. There are ways to get around this in FHS – for example, link directly to /lib/libudev.so.1.6.3 or use /bin/python3.7 in your shebang. However, there are still a lot of unknowns.
This means that if we want to get any reproducibility and consistency, FHS does not work since there is no way to infer a lot of properties of a given file.
One solution is tools like Docker, Snap, and Flatpak that create isolated FHS environments containing fixed versions of all the dependencies of a given application, and distribute those environments. However, this solution has a host of problems.
What if we want to apply different configure flags to our application or change one of the dependencies? There is no guarantee that you would be able to get the build artifact from build instructions, since putting all the build artifacts in an isolated container guarantees consistency, not reproducibility, because during build-time, tools from host’s FHS are often used, and besides the dependencies that come from other isolated environments might change.
For example, two people using the same docker image will always get the same results, but two people building the same Dockerfile can (and often do) end up with two different images.
1.3. Drawbacks of Nix (on macOS)
The biggest issue with Nix on darwin is that NixOS (and Nix on linux) takes priority. This means:
- Apps aren’t guaranteed to build on macOS
- External dependencies and overlays (e.g. home-manager) aren’t guaranteed to work perfectly on darwin
- GUI application support is almost nonexistent
MacOS is also quite locked down compared to linux, which limits the customization you can do. You also need nix-darwin to manage flake configurations and macOS settings. Be prepared for nix (and other package managers) to break in a future macOS update. On top of this, aarch64-darwin is a Tier 4 platform, if packages that are failing the test aren’t critical, they get merged. You will run into packages that don’t run on m1 at all, and will likely have to PR or open an issue to get them fixed. Lastly, remember that aarch64-darwin is fairly new. Especially if you use the stable channel, expect to have to build the majority of packages from source. Even if you use the unstable/master channels, you will likely end up building some packages from source
1.4. Nix vs Homebrew, Pkgsrc, and Macports
The main package managers on macOS are:
Pkg Manager | Pkg Availability | Pkg Freshness | Ease of Use | Features | Performance |
---|---|---|---|---|---|
Nix (Unstable) | 5 | 5 | 2.5 | 4.5 | 4 |
Macports | 3.5 | 3 | 3 | 3 | 4 |
Pkgsrc | 2 | 2 | 2 | 4 | 5 |
Homebrew | 3.5 | 4.5 | 4.5 | 2 | 2 |
Package management on macOS has a somewhat complex history, mostly owing to the fact that unlike most Linux distributions, macOS does not ship with a default package manager out of the box. It’s not surprising that one of the first projects to solve the problem of package management, Fink, was created very early, with its initial releases predating that of Mac OS X 10.0 by several months. Using Debian’s dpkg and apt as its backend, Fink is still actively maintained, though I haven’t looked at it very closely.
MacPorts, on the other hand, was released in 2002 as part of OpenDarwin, while Homebrew was released seven years later as a “solution” to many of the shortcomings that the author saw in MacPorts. Pkgsrc is an older package manager for UNIX-like systems, and supports several BSD’s, as well as Linux and MacOS. Nix is a cross-platform package manager that utilizes a purely functional deployment model where software is installed into unique directories generated through cryptographic hashes. It is also the name of the tool’s programming language. A package’s hash takes into account the dependencies. This package management model advertises more reliable, reproducible, and portable packages.
Homebrew makes several questionable design decisions, but one of these deserves its own section: the choice to explicitly eschew root (in fact, it will refuse to work at all if run this way). This fundamentally is a very bad idea: package managers that install software for all users of your computer, as Homebrew does by default, should always require elevated privileges to function correctly. This decision has important consequences for both security and usability, especially with the advent of System Integrity Protection in OS X El Capitan.
For quite a while, Homebrew essentially considered itself the owner of /usr/local (both metaphorically and literally, as it would change the permissions of the directory), to the point where it would do things like plop its README down directly into this folder. After rootless was introduced, it moved most of its files to subdirectories; however, to maintain the charade of “sudo-less” installation, Homebrew will still trash the permissions of folders inside /usr/local. Homebrew’s troubleshooting guide lists these out, because reinstalling macOS sets the permissions back to what they’re supposed to be and breaks Homebrew in the process.
If commands fail with permissions errors, check the permissions of /usr/local’s subdirectories. If you’re unsure what to do, you can run cd /usr/local && sudo chown -R $(whoami) bin etc include lib sbin share var opt Cellar Caskroom Frameworks.
MacPorts, on the other hand, swings so far in the other direction that it’s actually borderline inconvenient to use in some sense. Philosophically, MacPorts has a very different perspective of how it should work: it tries to prevent conflicts with the system as much as possible. To achieve this, it sets up a hierarchy under /opt (which is the annoying bit, because this directory is not on $PATH by default, nor is picked up by compilers without some prodding).
Of course, this design means that there is a single shared installation is among users, so running port requires elevated privileges whenever performing an operation that affects all users (which, admittedly, is most of the time). MacPorts is smart about this, though: it will shed permissions and run as the macports user whenever possible.
In line with their stated philosophy to prevent conflicts with macOS, MacPorts will set up its own tools in isolation from those provided by the system (in fact, builds run in “sandboxes” under the macports user, where attempts to access files outside of the build directory–which includes system tools–are intercepted and blocked). This means MacPorts needs to install some “duplicate” tools (whereas Homebrew will try to use the ones that come with your system where possible), the downside of which is that there is an one-time “up-front” cost as it installs base packages. The upside is that this approach is significantly more contained, which makes it easier to manage and more likely to continue working as macOS changes under it.
Finally, MacPorts just seems to have a lot of thought put into it with regards to certain aspects: for example, the MacPorts Registry database is backed by SQLite by default, which makes easily introspectable in case something goes wrong. Another useful feature is built-in “livechecks” for most ports, which codify upstream version checks and make it easy to see when MacPorts’s package index need to be updated.
I won’t delve too much into why I choose nix in the end (as I’ve covered it before), but I feel like nix takes the best of both worlds and more. You have the ease of use that homebrew provides, the sandboxing and though that was put into MacPorts, while having excellent sandboxing and the seperate nixbld user.
2. Installing and notes
NOTE: These are available as an executable script ./extra/install.sh
Install Nix. I have it setup for multi-user, but you can remove the --daemon if you want a single user install
Launch an ephemeral shell with git, nixUnstable, and Emacs
Tangle the .org files (not needed, but recommend in case I forgot to update tangled files)
bash
git clone --depth 1 https://github.com/shaunsingh/nix-darwin-dotfiles.git ~/nix-darwin-dotfiles/ && cd nix-darwin-dotfiles emacs --batch --eval "(progn (require 'org) (setq org-confirm-babel-evaluate nil) (org-babel-tangle-file \"~/nix-darwin-dotfiles/nix-config.org\"))" emacs --batch --eval "(progn (require 'org) (setq org-confirm-babel-evaluate nil) (org-babel-tangle-file \"~/nix-darwin-dotfiles/configs/doom/config.org\"))"
(if emacs asks you for comment syntax, put `# ` for everything) Build, and switch to the dotfiles
bash
nix build ~/nix-darwin-dotfiles\#darwinConfigurations.shaunsingh-laptop.system --extra-experimental-features nix-command --extra-experimental-features flakes
./result/sw/bin/darwin-rebuild switch --flake .#shaunsingh-laptop
(note, --extra-experimental-features is only needed the first time around. After that the configuration will edit /etc/nix/nix.conf to enable flakes and nix-command by default) Symlinking with nix (and managing doom with nix-doom-emacs) is very finicky, so for now we need to manually symlink them
Install doom emacs
bash
git clone --depth 1 https://github.com/hlissner/doom-emacs ~/.config/emacs ~/.config/emacs/bin/doom install
2.1. Using Nix unstable OOTB
If you want to use nix unstable out of of the box then you can use the following script
bash
RELEASE="nix-2.5pre20211019_4a2b7cc" URL="https://github.com/numtide/nix-unstable-installer/releases/download/$RELEASE/install" # install using workaround for darwin systems if [[ $(uname -s) = "Darwin" ]]; then FLAG="--darwin-use-unencrypted-nix-store-volume" fi [[ ! -z "$1" ]] && URL="$1" if command -v nix > /dev/null; then echo "nix is already installed on this system." else bash <(curl -L $URL) --daemon $FLAG fi
2.2. Additional Configuration
2.2.1. Emacs
If you want to use Emacs-NG, use the following build options
bash
git clone --depth 1 https://github.com/emacs-ng/emacs-ng.git cd emacs-ng ./autogen.sh ./configure CFLAGS="-Wl,-rpath,shared,--disable-new-dtags -g -O3 -mtune=native -march=native -fomit-frame-pointer" \ --prefix=/usr/local/ \ --with-json --with-modules --with-compress-install \ --with-threads --with-included-regex --with-zlib --with-libsystemd \ --with-rsvg --with-native-compilation --with-webrender --without-javascript \ --without-sound --without-imagemagick --without-makeinfo --without-gpm --without-dbus \ --without-pop --without-toolkit-scroll-bars --without-mailutils --without-gsettings \ --with-all make -j$(($(nproc) * 2)) NATIVE_FULL_AOT=1 make install-strip
If you want to update the doom configuration, you can run
If you modify your shell configuration, please do run doom env to regenerate env vars
- Mu4e and Gmail
Email will have a few issues, since its hardcoded to my account. Replace instances of my name and email in ~/.doom.d/config.org Indexed mail will go under ~/.mbsync/, you can either manually run mbsync or use emacs to update mail.
- Org Mode
My org mode config includes two additional plugins, org-agenda and org-roam. Both these plugins need a set directory. All org files can go under the created ~/org dir. Roam files go under ~/org/roam
2.2.2. Fonts
SFMono must be installed seperately due to liscensing issues, all other fonts are managed via nix.
2.2.3. Neovim
Run :PackerSync to install packer and plugins. Run :checkhealth to check for possible issues. If you want to take advantage of the LSP and/or treesitter, you can install language servers and parsers using the following command: :LspInstall (language) :TSInstall (language) NOTE: If you want to use neorg’s treesitter parser on macOS, you need to link GCC to CC. Instructions here. I also recommend installing Neovide
3. Flakes
3.1. Why Flakes
Once upon a time, Nix pioneered reproducible builds: it tries hard to ensure that two builds of the same derivation graph produce an identical result. Unfortunately, the evaluation of Nix files into such a derivation graph isn’t nearly as reproducible, despite the language being nominally purely functional.
For example, Nix files can access arbitrary files (such as ~/.config/nixpkgs/config.nix), environment variables, Git repositories, files in the Nix search path ($NIX_PATH), command-line arguments (--arg) and the system type (builtins.currentSystem). In other words, evaluation isn’t as hermetic as it could be. In practice, ensuring reproducible evaluation of things like NixOS system configurations requires special care.
Furthermore, there is no standard way to compose Nix-based projects. It’s rare that everything you need is in Nixpkgs; consider for instance projects that use Nix as a build tool, or NixOS system configurations. Typical ways to compose Nix files are to rely on the Nix search path (e.g. import <nixpkgs>) or to use fetchGit or fetchTarball. The former has poor reproducibility, while the latter provides a bad user experience because of the need to manually update Git hashes to update dependencies.
There is also no easy way to deliver Nix-based projects to users. Nix has a “channel” mechanism (essentially a tarball containing Nix files), but it’s not easy to create channels and they are not composable. Finally, Nix-based projects lack a standardized structure. There are some conventions (e.g. shell.nix or release.nix) but they don’t cover many common use cases; for instance, there is no way to discover the NixOS modules provided by a repository.
Flakes are a solution to these problems. A flake is simply a source tree (such as a Git repository) containing a file named flake.nix that provides a standardized interface to Nix artifacts such as packages or NixOS modules. Flakes can have dependencies on other flakes, with a “lock file” pinning those dependencies to exact revisions to ensure reproducible evaluation.
When you clone this flake and install it, your system should theoretically be the exactly the same as mine, down to the commit of nixpkgs. There are also other benefits, such as that nix evaluations are cached.
3.2. Notes on using the flake
When you install this config, there are 3 useful commands you need to know
- Updating the flake. This will update the flake.lock lockfile to the latest commit of nixpkgs, emacs-overlay, etc
- Building and Installing the flake. This will first build and download everything you need, then rebuild your machine, so it “installs”
bash
nix build ~/nix-darwin-dotfiles\#darwinConfigurations.shaunsingh-laptop.system --extra-experimental-features nix-command --extra-experimental-features flakes
./result/sw/bin/darwin-rebuild switch --flake .#shaunsingh-laptop
- Testing the flake. If you have any errors when you play around with this config, then this will let you know what went wrong.
The flake.nix below does the following:
- Add a binary cache for nix-community overlays
- Add inputs (nixpkgs-master, nix-darwin, home-manager, and spacebar)
- Add overlays to get the latest versions of neovim (nightly) and emacs (emacs29)
- Create a nix-darwin configuration for my hostname
- Source the mac, home, and pam modules
- Configure home-manager and the nix-daemon
- Enable the use of touch-id for sudo authentication
- Configure nixpkgs to use the overlays above, and allow unfree packages
- Configure nix to enable flakes and nix-command by default, and add x86-64-darwin as a platform (to install packages through rosetta)
- Install my packages and config dependencies
- Install the required fonts
nix
{ description = "Shaurya's Nix Environment"; nixConfig = { # Add binary cache for neovim-nightly/emacsGcc extra-substituters = [ "https://cachix.cachix.org" "https://nix-community.cachix.org" ]; extra-trusted-public-keys = [ "cachix.cachix.org-1:eWNHQldwUO7G2VkjpnjDbWwy4KQ/HNxht7H4SSoMckM=" "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" ]; }; inputs = { # All packages should follow latest nixpkgs unstable.url = "github:nixos/nixpkgs/master"; nur.url = "github:nix-community/NUR"; darwin = { url = "github:LnL7/nix-darwin/master"; inputs.nixpkgs.follows = "unstable"; }; home-manager = { url = "github:nix-community/home-manager/master"; inputs.nixpkgs.follows = "unstable"; }; # Bar spacebar = { url = "github:shaunsingh/spacebar/master"; inputs.nixpkgs.follows = "unstable"; }; # Editors neovim = { url = "github:nix-community/neovim-nightly-overlay"; inputs.nixpkgs.follows = "unstable"; }; emacs = { url = "github:shaunsingh/emacs"; inputs.nixpkgs.follows = "unstable"; }; # overlays rust-overlay = { url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "unstable"; }; nixpkgs-s2k = { url = "github:shaunsingh/nixpkgs-s2k"; inputs.nixpkgs.follows = "unstable"; }; }; outputs = { self, nixpkgs, nixpkgs-s2k, darwin, home-manager, ... }@inputs: { darwinConfigurations."shaunsingh-laptop" = darwin.lib.darwinSystem { system = "aarch64-darwin"; modules = [ { nixpkgs.overlays = [ nixpkgs-s2k.overlay ]; } ./modules/mac.nix ./modules/home.nix ./modules/pam.nix home-manager.darwinModule { home-manager = { useGlobalPkgs = true; useUserPackages = true; }; } ({ pkgs, lib, ... }: { services.nix-daemon.enable = true; security.pam.enableSudoTouchIdAuth = true; nixpkgs = { overlays = with inputs; [ nur.overlay spacebar.overlay neovim.overlay emacs.overlay rust-overlay.overlay ]; config.allowUnfree = true; }; nix = { package = pkgs.nix; extraOptions = '' system = aarch64-darwin extra-platforms = aarch64-darwin x86_64-darwin experimental-features = nix-command flakes build-users-group = nixbld ''; }; environment.systemPackages = with pkgs; [ # Emacs deps ((emacsPackagesNgGen emacs).emacsWithPackages (epkgs: [ epkgs.vterm ])) ## make sure ripgrep supports pcre2 (for vertico) (ripgrep.override { withPCRE2 = true; }) sqlite ## Required for plots but not installed by default gnuplot # pandoc ## Required for dictionaries but not installed by default # sdcv (aspellWithDicts (ds: with ds; [ en en-computers en-science ])) (texlive.combine { inherit (texlive) scheme-small dvipng dvisvgm l3packages xcolor soul adjustbox collectbox amsmath siunitx cancel mathalpha capt-of chemfig wrapfig mhchem fvextra cleveref latexmk tcolorbox environ arev amsfonts simplekv alegreya sourcecodepro newpx svg catchfile transparent hanging biblatex biblatex-mla; }) # Language Deps ## Build Tools jdk luajit rust-bin.nightly.latest.default ## Language Servers nodePackages.pyright rust-analyzer languagetool ## Formatting nixfmt black shellcheck # Terminal utils and rust alternatives :tm: uutils-coreutils xcp lsd procs tree fd zoxide bottom discocss # eww ]; fonts = { enableFontDir = true; fonts = with pkgs; [ overpass alegreya alegreya-sans emacs-all-the-icons-fonts sf-mono-liga-bin ]; }; }) ]; }; }; }
4. Modules
4.1. Home.nix
Home Manager allows you to use Nix’s declarative approach to manage your user-level configuration and packages. It works on any *nix system supported by Nix, including MacOS.
4.1.1. Doom-emacs
Nix via doom-emacs is very, very annoying. Initially I was using Nix-doom-emacs. However, this has a few drawbacks
- It doesn’t support straight :recipe, so all packages must be from melpa or elpa
- It pins the version of doom, so you need to update doom and its dependencies painstakingly manually
- It just ends up breaking anyways.
A simpler solution is just to have nix clone doom-emacs to ~/.config/emacs, and the user can handle doom manually
nix
# home-manager.users.shauryasingh.home.file = { # "~/.config/doom" = { # recursive = true; # source = ../configs/doom; # }; # };
4.1.2. Git
As opposed to what the xcode CLT provides, I want lfs enabled with git, and use delta instead of the default diff tool (rust alternatives go brr). MacOS is also quite annoying with its .DS_Store’s everywhere, so lets ignore that
nix
home-manager.users.shauryasingh.programs.git = { enable = true; userName = "shaunsingh"; userEmail = "shaunsingh0207@gmail.com"; lfs.enable = true; delta = { enable = true; options = { syntax-theme = "Nord"; line-numbers = true; width = 1; navigate = false; hunk-header-style = "file line-number syntax"; hunk-header-decoration-style = "bold black"; file-modified-label = "modified:"; zero-style = "dim"; minus-style = "bold red"; minus-non-emph-style = "dim red"; minus-emph-style = "bold red"; minus-empty-line-marker-style = "normal normal"; plus-style = "green normal bold"; plus-non-emph-style = "dim green"; plus-emph-style = "bold green"; plus-empty-line-marker-style = "normal normal"; whitespace-error-style = "reverse red"; }; }; ignores = [ ".dir-locals.el" ".envrc" ".DS_Store" ]; };
4.1.3. IdeaVim
Intellij Idea ships with a very nice Vim emulation plugin. This is configured via a vimrc-like file (~/.ideavimrc). Since it doesn’t have proper support in home-manger, we can just generate a file and symlink it into place
nix
home-manager.users.shauryasingh.home.file = { ".ideavimrc".text = '' " settings set ignorecase set smartcase set scrolloff=3 " 3 lines above/below cursor when scrolling set nonumber set clipboard+=unnamed set multiple-cursors set numberwidth=2 set expandtab=true set shiftwidth=4 " plugins set easymotion set NERDTree set surround set highlightedyank " bindings let mapleader = " " nmap <leader>. :action GotoFile<cr> nmap <leader>fr :action RecentFiles<cr> nmap <leader>ww <Plug>(easymotion-w) nmap <leader>tz :action Enter Zen Mode<cr> nmap <leader>op :NERDTreeToggle<cr> nmap <leader>ot :Terminal<cr> nmap <leader>: :action SearchEverywhere<cr> nmap <leader>/ :action Find<cr> " use ; to enter command nmap ; : " use jk for escaping inoremap jk <Esc> cnoremap jk <Esc> " move by visual lines" nmap j gj nmap k gk " use C-hjkl to navigate splits nmap <C-h> <c-w>h nmap <C-l> <c-w>l nmap <C-k> <c-w>k nmap <C-j> <c-w>j nmap <leader>E :action Tool_External Tools_emacsclient<cr> '';
4.1.4. Discocss
Discocss is a way to inject custom CSS into discord. Similar to ideavim, it doesn’t have proper support but we can generate a file for ~/.config/discocss/custom.css
nix
".config/discocss/custom.css".text = '' /* Discord Nord https://github.com/joinemm/discord-css */ /* define colors */ :root { --background-primary: #242730; --background-secondary: #2a2e38; --background-secondary-alt: #2a2e38; --background-tertiary: #242730; --background-accent: #242730; --channeltextarea-background: #242730; --text-normal: #6c605a; --text-muted: #ce9c85; --channels-default: #a09c80; --interactive-normal: #242730; --interactive-hover: #a09c80; --interactive-active: #a09c80; --interactive-muted: #665c54; --header-primary: #6c605a; --header-secondary: #5c605a; --background-floating: #242730; --scrollbar-auto-thumb: #1d2021; --scrollbar-auto-track: #3c3836; --text-link: #8f8678; --selection: #92837480; } * { font-family: Liga SFMono Nerd Font; font-size: 11px; } /* main backgrounds */ .scrollerThemed-2oenus.themeDark-2cjlUp.scrollerTrack-1ZIpsv>.scroller-2FKFPG::-webkit-scrollbar-track, .scrollerThemed-2oenus.themeDark-2cjlUp.scrollerTrack-1ZIpsv>.scrollerStore-390omS::-webkit-scrollbar-track, .theme-dark .scrollerWrap-2lJEkd.scrollerTrack-1ZIpsv>.scroller-2FKFPG::-webkit-scrollbar-track, .theme-dark .scrollerWrap-2lJEkd.scrollerTrack-1ZIpsv>.scrollerStore-390omS::-webkit-scrollbar-track, .theme-dark .da-messageGroupWrapper, .theme-dark .bodyInner-245q0L, .theme-dark .bottomDivider-1K9Gao, .theme-dark .headerNormal-T_seeN, .theme-dark .root-1gCeng, .tabBody-3YRQ8W, .theme-dark .container-1D34oG .theme-dark .uploadModal-2ifh8j, .theme-dark .modal-yWgWj-, .uploadModal-2ifh8j, .theme-dark .emojiAliasInput-1y-NBz .emojiInput-1aLNse, .theme-dark .selected-1Tbx07, .theme-dark .option-96V44q.selected-rZcOL- { background-color: var(--background-primary) !important; } .da-popouts .da-container, .da-friendsTableHeader, .da-friendsTable, .da-autocomplete, .da-themedPopout, .da-footer, .da-userPopout>*, .da-autocompleteHeaderBackground, .theme-dark .bar-2Qqk5Z, .theme-dark .payment-xT17Mq, .theme-dark .paymentPane-3bwJ6A, .theme-dark .paginator-166-09, .theme-dark .codeRedemptionRedirect-1wVR4b, .theme-dark .scrollerThemed-2oenus.themedWithTrack-q8E3vB .scroller-2FKFPG::-webkit-scrollbar-thumb, .theme-dark .footer-3mqk7D, .theme-dark .footer-2gL1pp, .scrollerThemed-2oenus.themeDark-2cjlUp .scroller-2FKFPG::-webkit-scrollbar-thumb, .theme-dark .scrollerWrap-2lJEkd .scroller-2FKFPG::-webkit-scrollbar-thumb, .theme-dark .inset-3sAvek, .theme-dark .quickMessage-1yeL4E, .wrapper-2aW0bm, .theme-dark .autocomplete-1vrmpx, .searchBar-3dMhjb, .theme-dark .body-3iLsc4, .theme-dark .footer-2gL1pp, .theme-dark .footer-1fjuF6, .cardPrimary-1Hv-to, .theme-dark .card-FDVird:before, .theme-dark .colorPickerCustom-2CWBn2 { background-color: var(--background-secondary); } /* scrollbars */ .da-messagesWrapper .da-scroller::-webkit-scrollbar, .da-messagesWrapper .da-scroller::-webkit-scrollbar-track-piece { background-color: var(--background-tertiary) !important; border-color: rgba(0, 0, 0, 0) !important; } .da-scrollerThemed .da-scroller::-webkit-scrollbar-thumb, .da-scrollerWrap .da-scroller::-webkit-scrollbar-thumb { background-color: var(--background-secondary) !important; border-color: var(--background-tertiary) !important; } .theme-dark .scrollerThemed-2oenus.themedWithTrack-q8E3vB .scroller-2FKFPG::-webkit-scrollbar-track-piece { background-color: var(--background-primary) !important; border-color: rgba(0, 0, 0, 0) !important; } .theme-dark .selectorSelected-1_M1WV, .newMessagesBar-mujexs, .theme-dark .searchAnswer-3Dz2-q, .theme-dark .searchFilter-2ESiM3, .theme-dark .progress-1IcQ3A, .themeDefault-24hCdX, .theme-dark .lookFilled-1Gx00P.colorPrimary-3b3xI6 { background-color: var(--background-accent); } .theme-dark .container-3ayLPN { background-color: var(--background-tertiary); } .theme-dark .option-96V44q.selected-rZcOL- { background-color: var(--background-floating); } .theme-dark .pageIndicator-1gAbyA, .theme-dark .pageButtonNext-V2kUq0, .theme-dark .pageButtonPrev-1Y-47D { border-color: var(--background-accent); } .scroller-2FKFPG::-webkit-scrollbar, .barButtonBase-3UKlW2, .theme-dark .scrollerThemed-2oenus.themeGhostHairline-DBD-2d .scroller-2FKFPG::-webkit-scrollbar-thumb { background: transparent; } /* remove gradients */ .theme-dark .da-option:after, .theme-dark .option-96V44q:after { background-image: none !important; } /* search text */ .theme-dark .searchOption-zQ-1l6 .answer-1n6g43, .theme-dark .option-96V44q .filter-3Y_im- { color: var(--text-muted) } .theme-dark .searchOption-zQ-1l6 .filter-3Y_im- { color: var(--text-normal) } /* side panel bottom section border */ .panels-j1Uci_ { border-top: 2px solid var(--background-modifier-accent); } .container-1giJp5 { border-width: 2px; } /* selected text */ ::selection { background: var(--selection); } /* hide that stupid nitro gift button */ .button-38aScr[aria-label="Send a gift"] { display: none; } /* hide blocked messages */ [class="groupStart-23k01U"] { display: none; } /* enhanceddiscord server count text */ .theme-dark .keybind-KpFkfr { color: var(--channels-default) } /* unloaded emojis */ .theme-dark .imageLoading-bpSr0M { background-image: none; background: var(--background-primary); border-radius: 50%; } .sprite-2iCowe { filter: none !important; } /* Doom style (dark) (c) Pavel Pertsev (original style at https://github.com/morhetz/gruvbox) */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #242730; } .hljs, .hljs-subst { color: #ebdbb2; } /* Doom Red */ .hljs-deletion, .hljs-formula, .hljs-keyword, .hljs-link, .hljs-selector-tag { color: #fb4934; } /* Doom Blue */ .hljs-built_in, .hljs-emphasis, .hljs-name, .hljs-quote, .hljs-strong, .hljs-title, .hljs-variable { color: #83a598; } /* Doom Yellow */ .hljs-attr, .hljs-params, .hljs-template-tag, .hljs-type { color: #fabd2f; } /* Doom Purple */ .hljs-builtin-name, .hljs-doctag, .hljs-literal, .hljs-number { color: #8f3f71; } /* Doom Orange */ .hljs-code, .hljs-meta, .hljs-regexp, .hljs-selector-id, .hljs-template-variable { color: #fe8019; } /* Doom Green */ .hljs-addition, .hljs-meta-string, .hljs-section, .hljs-selector-attr, .hljs-selector-class, .hljs-string, .hljs-symbol { color: #b8bb26; } /* Doom Aqua */ .hljs-attribute, .hljs-bullet, .hljs-class, .hljs-function, .hljs-function .hljs-keyword, .hljs-meta-keyword, .hljs-selector-pseudo, .hljs-tag { color: #8ec07c; } /* Doom Gray */ .hljs-comment { color: #928374; } /* Doom Purple */ .hljs-link_label, .hljs-literal, .hljs-number { color: #d3869b; } .hljs-comment, .hljs-emphasis { font-style: italic; } .hljs-section, .hljs-strong, .hljs-tag { font-weight: bold; } /* Auto Hide by \xynstr#0300 */ /* Transition */ .sidebar-2K8pFh:hover { width: 240px !important; transition: 0.1s ease !important; transition-delay: 0.3s !important; } /* Detects screen size to show channels if screen is big enough */ @media screen and (max-width: 1100px) { .sidebar-2K8pFh { width: 0px !important; transition: 0.3s ease !important; position: fixed !important; height: calc(100% - 47.988px) !important; z-index: 1 !important; bottom: 0px !important; } .wrapper-3NnKdC:hover + .base-3dtUhz > .content-98HsJk > .sidebar-2K8pFh { width: 240px !important; } .sidebar-2K8pFh:hover { width: 240px !important; ''; };
4.1.5. Firefox
Although safari is my main browser, firefox looks very appealing with its excellent privacy and speed
GUI apps are very finicky with nix, and so I create a fake package so that we can still use the configuration from home-manager without having to install it via nix. The user can then install firefox manually to ~/Applications
nix
home-manager.users.shauryasingh.programs.firefox.package = pkgs.runCommand "firefox-0.0.0" { } "mkdir $out"; home-manager.users.shauryasingh.programs.firefox.extensions = with pkgs.nur.repos.rycee.firefox-addons; [ ublock-origin tridactyl duckduckgo-privacy-essentials reddit-enhancement-suite betterttv ];
Now for the configuration. We want firefox to use the css at ./configs/userChrome.css, and we want to configure the UI. Lets also enable the (rust powered ftw) webrender/servo renderer.
nix
home-manager.users.shauryasingh.programs.firefox.profiles = let userChrome = builtins.readFile ../configs/userChrome.css; settings = { "app.update.auto" = true; "browser.startup.homepage" = "https://tilde.cade.me"; "browser.search.region" = "US"; "browser.search.countryCode" = "US"; "browser.ctrlTab.recentlyUsedOrder" = false; "browser.newtabpage.enabled" = false; "browser.bookmarks.showMobileBookmarks" = true; "browser.uidensity" = 1; "browser.urlbar.placeholderName" = "SearX"; "browser.urlbar.update1" = true; "identity.fxaccounts.account.device.name" = config.networking.hostName; "privacy.trackingprotection.enabled" = true; "privacy.trackingprotection.socialtracking.enabled" = true; "privacy.trackingprotection.socialtracking.annotate.enabled" = true; "reader.color_scheme" = "sepia"; "services.sync.declinedEngines" = "addons,passwords,prefs"; "services.sync.engine.addons" = false; "services.sync.engineStatusChanged.addons" = true; "services.sync.engine.passwords" = false; "services.sync.engine.prefs" = false; "services.sync.engineStatusChanged.prefs" = true; "signon.rememberSignons" = false; "gfx.webrender.all" = true; "toolkit.legacyUserProfileCustomizations.stylesheets" = true; }; in { home = { inherit settings; inherit userChrome; id = 0; }; };
And of course for the css itself
CSS
/* * Hide window controls */ .titlebar-buttonbox-container{ display: none !important; } .titlebar-placeholder, #TabsToolbar .titlebar-spacer{ display: none; } #navigator-toolbox::after{ display: none !important; } /* * Hide all the clutter in the navbar */ #main-window :-moz-any(#back-button, #forward-button, #stop-reload-button, #home-button, #library-button, #sidebar-button, #star-button, #pocket-button, #permissions-granted-icon, #fxa-toolbar-menu-button, #_d7742d87-e61d-4b78-b8a1-b469842139fa_-browser-action, /* Vimium */ #ublock0_raymondhill_net-browser-action) { display: none !important; } /* * Hide tabs if only one tab */ #titlebar .tabbrowser-tab[first-visible-tab="true"][last-visible-tab="true"]{ display: none !important; } /* * Minimal theme */ #navigator-toolbox toolbarspring { display: none; } /* Hide filler */ #customizableui-special-spring2{ display:none; } .tab-background{ padding-bottom: 0 !important; } #navigator-toolbox #urlbar-container { padding: 0 !important; margin: 0 !important; } #navigator-toolbox #urlbar { border: none !important; border-radius: 0 !important; box-shadow: none !important; } #navigator-toolbox #PanelUI-button { padding: 0 !important; margin: 0 !important; border: none !important; } #navigator-toolbox #nav-bar { box-shadow: none !important; } #navigator-toolbox #pageActionButton { display: none; } #navigator-toolbox #pageActionSeparator { display: none; } #fullscr-toggler { height: 0 !important; } #navigator-toolbox .urlbar-history-dropmarker { display: none; } #navigator-toolbox #tracking-protection-icon-container { padding-right: 0 !important; border: none !important; display: none !important; } #navigator-toolbox .tab-close-button, #navigator-toolbox #tabs-newtab-button { display: none; } #navigator-toolbox #urlbar { padding: 0 !important; padding-left: 1ch !important; font-size: 13px; } #navigator-toolbox #urlbar-background { border: none !important; margin: 0 !important; } #navigator-toolbox .toolbarbutton-1 { width: 22px; } #navigator-toolbox .tabbrowser-tab { font-size: 14px } #navigator-toolbox .tab-background { box-shadow: none!important; border: none !important; } #navigator-toolbox .tabbrowser-tab::after { display: none !important; } #navigator-toolbox #urlbar-zoom-button { border: none !important; } #appMenu-fxa-container, #appMenu-fxa-container + toolbarseparator { display: none !important; } #sync-setup { display: none !important; } /* * Hamburger menu to the left */ #PanelUI-button { -moz-box-ordinal-group: 0; border-left: none !important; border-right: none !important; position: absolute; } #toolbar-context-menu .viewCustomizeToolbar { display: none !important; } :root[uidensity=compact] #PanelUI-button { margin-top: -30px; } #PanelUI-button { margin-top: -30px; } :root[uidensity=touch] #PanelUI-button { margin-top: -36px; } /* * Tabs to the right of the urlbar */ /* Modify these to change relative widths or default height */ #navigator-toolbox{ --uc-navigationbar-width: 40vw; --uc-toolbar-height: 40px; } /* Override for other densities */ :root[uidensity="compact"] #navigator-toolbox{ --uc-toolbar-height: 30px; } :root[uidensity="touch"] #navigator-toolbox{ --uc-toolbar-height: 40px; } :root[uidensity=compact] #urlbar-container.megabar{ --urlbar-container-height: var(--uc-toolbar-height) !important; padding-block: 0 !important; } :root[uidensity=compact] #urlbar.megabar{ --urlbar-toolbar-height: var(--uc-toolbar-height) !important; } /* prevent urlbar overflow on narrow windows */ /* Dependent on how many items are in navigation toolbar ADJUST AS NEEDED */ @media screen and (max-width: 1300px){ #urlbar-container{ min-width:unset !important } } #TabsToolbar{ margin-left: var(--uc-navigationbar-width); } #tabbrowser-tabs{ --tab-min-height: var(--uc-toolbar-height) !important; } /* This isn't useful when tabs start in the middle of the window */ .titlebar-placeholder[type="pre-tabs"], .titlebar-spacer[type="pre-tabs"]{ display: none } #navigator-toolbox > #nav-bar{ margin-right:calc(100vw - var(--uc-navigationbar-width)); margin-top: calc(0px - var(--uc-toolbar-height)); } /* Zero window drag space */ :root[tabsintitlebar="true"] #nav-bar{ padding-left: 0px !important; padding-right: 0px !important; } /* 1px margin on touch density causes tabs to be too high */ .tab-close-button{ margin-top: 0 !important } /* Hide dropdown placeholder */ #urlbar-container:not(:hover) .urlbar-history-dropmarker{ margin-inline-start: -30px; } /* Fix customization view */ #customization-panelWrapper > .panel-arrowbox > .panel-arrow{ margin-inline-end: initial !important; }
4.1.6. Alacritty
Alacritty is my terminal emulator of choice. Similar to firefox, we want to create a fake package, and then configure it as normal
nix
home-manager.users.shauryasingh.programs.alacritty = { enable = true; # We need to give it a dummy package package = pkgs.runCommand "alacritty-0.0.0" { } "mkdir $out"; settings = { window.padding.x = 45; window.padding.y = 45; window.decorations = "buttonless"; window.dynamic_title = true; live_config_reload = true; mouse.hide_when_typing = true; use_thin_strokes = true; cursor.style = "Beam"; font = { size = 14; normal.family = "Liga SFMono Nerd Font"; normal.style = "Light"; bold.family = "Liga SFMono Nerd Font"; bold.style = "Bold"; italic.family = "Liga SFMono Nerd Font"; italic.style = "Italic"; }; colors = { cursor.cursor = "#bbc2cf"; primary.background = "#242730"; primary.foreground = "#bbc2cf"; normal = { black = "#2a2e38"; red = "#ff665c"; green = "#7bc275"; yellow = "#FCCE7B"; blue = "#5cEfFF"; magenta = "#C57BDB"; cyan = "#51afef"; white = "#bbc2cf"; }; bright = { black = "#484854"; red = "#ff665c"; green = "#7bc275"; yellow = "#fcce7b"; blue = "#5cefff"; magenta = "#c57bdb"; cyan = "#51afef"; white = "#bbc2cf"; }; }; }; };
4.1.7. Kitty
I no longer use kitty (its quite slow to start and has too many features I don’t need), but I keep the config around just in case
nix
# home-manager.users.shauryasingh.programs.kitty = { # enable = true; # package = builtins.path { # path = /Applications/kitty.app/Contents/MacOS; # filter = (path: type: type == "directory" || builtins.baseNameOf path == "kitty"); # }; # # enable = true; # settings = { # font_family = "Liga SFMono Nerd Font"; # font_size = "14.0"; # adjust_line_height = "120%"; # disable_ligatures = "cursor"; # hide_window_decorations = "yes"; # scrollback_lines = "50000"; # cursor_blink_interval = "0.5"; # cursor_stop_blinking_after = "10.0"; # window_border_width = "0.7pt"; # draw_minimal_borders = "yes"; # macos_option_as_alt = "no"; # cursor_shape = "beam"; # foreground = "#D8DEE9"; # background = "#2E3440"; # selection_foreground = "#000000"; # selection_background = "#FFFACD"; # url_color = "#0087BD"; # cursor = "#81A1C1"; # color0 = "#3B4252"; # color8 = "#4C566A"; # color1 = "#BF616A"; # color9 = "#BF616A"; # color2 = "#A3BE8C"; # color10 = "#A3BE8C"; # color3 = "#EBCB8B"; # color11 = "#EBCB8B"; # color4 = "#81A1C1"; # color12 = "#81A1C1"; # color5 = "#B48EAD"; # color13 = "#B48EAD"; # color6 = "#88C0D0"; # color14 = "#8FBCBB"; # color7 = "#E5E9F0"; # color15 = "#B48EAD"; # }; # };
4.1.8. Fish
I like to use the fish shell. Although it isn’t POSIX, it has the best autocompletion and highlighting I’ve seen.
nix
programs.fish.enable = true; environment.shells = with pkgs; [ fish ]; users.users.shauryasingh = { home = "/Users/shauryasingh"; shell = pkgs.fish; };
- Settings fish as default
On macOS nix doesn’t set the fish shell to the main shell by default (like it does on NixOS), so lets do that manually
nix
system.activationScripts.postActivation.text = '' # Set the default shell as fish for the user sudo chsh -s ${lib.getBin pkgs.fish}/bin/fish shauryasingh '';
- Aliases
I also like to alias common commands with other, better rust alternatives :tm:
nix
programs.fish.shellAliases = with pkgs; { ":q" = "exit"; vi = "emacsclient -c"; git-rebsae = "git rebase -i HEAD~2"; ll = "exa -lF --color-scale --no-user --no-time --no-permissions --group-directories-first --icons -a"; ls = "exa -lF --group-directories-first --icons -a"; ps = "ps"; tree = "tree -a -C"; cat = "bat"; top = "btm"; cp = "xcp"; find = "fd"; calc = "emacs -f full-calc"; neovide = "/Applications/Neovide.app/Contents/MacOS/neovide --frameless --multigrid"; nix-fish = "nix-shell --command fish"; };
- Prompt
I like to make my prompt look pretty (along with some nix-shell and git integration)
nix
programs.fish.promptInit = '' set -g fish_greeting "" set -U fish_color_autosuggestion brblack set -U fish_color_cancel -r set -U fish_color_command green set -U fish_color_comment magenta set -U fish_color_cwd green set -U fish_color_cwd_root red set -U fish_color_end magenta set -U fish_color_error red set -U fish_color_escape cyan set -U fish_color_history_current --bold set -U fish_color_host normal set -U fish_color_normal normal set -U fish_color_operator cyan set -U fish_color_param blue set -U fish_color_quote yellow set -U fish_color_redirection yellow set -U fish_color_search_match 'yellow' '--background=brightblack' set -U fish_color_selection 'white' '--bold' '--background=brightblack' set -U fish_color_status red set -U fish_color_user green set -U fish_color_valid_path --underline set -U fish_pager_color_completion normal set -U fish_pager_color_description yellow set -U fish_pager_color_prefix 'white' '--bold' '--underline' set -U fish_pager_color_progress 'white' '--background=cyan' # prompt set fish_prompt_pwd_dir_length 1 set __fish_git_prompt_show_informative_status 1 set fish_color_command green set fish_color_param $fish_color_normal set __fish_git_prompt_showdirtystate 'yes' set __fish_git_prompt_showupstream 'yes' set __fish_git_prompt_color_branch brown set __fish_git_prompt_color_dirtystate FCBC47 set __fish_git_prompt_color_stagedstate yellow set __fish_git_prompt_color_upstream cyan set __fish_git_prompt_color_cleanstate green set __fish_git_prompt_color_invalidstate red set __fish_git_prompt_char_dirtystate '~~' set __fish_git_prompt_char_stateseparator ' ' set __fish_git_prompt_char_untrackedfiles ' ...' set __fish_git_prompt_char_cleanstate '✓' set __fish_git_prompt_char_stagedstate '-> ' set __fish_git_prompt_char_conflictedstate "✕" set __fish_git_prompt_char_upstream_prefix "" set __fish_git_prompt_char_upstream_equal "" set __fish_git_prompt_char_upstream_ahead '>>=' set __fish_git_prompt_char_upstream_behind '=<<' set __fish_git_prompt_char_upstream_diverged '<=>' function _print_in_color set -l string $argv[1] set -l color $argv[2] set_color $color printf $string set_color normal end function _prompt_color_for_status if test $argv[1] -eq 0 echo magenta else echo red end end function fish_prompt set -l last_status $status set -l nix_shell_info ( if test -n "$IN_NIX_SHELL" echo -n " [nix-shell]" end ) if test $HOME != $PWD _print_in_color ""(prompt_pwd) blue end __fish_git_prompt " (%s)" _print_in_color "$nix_shell_info λ " (_prompt_color_for_status $last_status) ] end '';
- Init
I also want to disable the default greeting, and use tmux with fish. Lets also set nvim as the default editor, and add emacs to my path
nix
programs.fish.interactiveShellInit = '' set -g fish_greeting "" if not set -q TMUX tmux new-session -A -s main end zoxide init fish --cmd cd | source set -x EDITOR "nvim" set -x PATH ~/.config/emacs/bin $PATH '';
4.1.9. Neovim
Lastly, I didn’t feel like nix-ifying my neovim lua config. Lets cheat a bit and just symlink it instead
nix
home-manager.users.shauryasingh.programs.neovim = { enable = true; package = pkgs.neovim-nightly; vimAlias = true; extraPackages = with pkgs; [ tree-sitter # neovide-git nodejs tree-sitter ]; extraConfig = builtins.concatStringsSep "\n" [ '' lua << EOF ${lib.strings.fileContents ../configs/nyoom.nvim/nix.lua} EOF '' ]; };
4.1.10. Bat
Bat is another rust alternative :tm: to cat, and provides syntax highlighting. Lets theme it to match nord
nix
home-manager.users.shauryasingh.programs.bat = { enable = true; config = { theme = "Nord"; }; };
4.1.11. Tmux
Lastly, lets make tmux look just as pretty as our prompt, and enable truecolor support.
nix
programs.tmux.enable = true; programs.tmux.extraConfig = '' # make sure fish works in tmux set -g default-terminal "screen-256color" set -sa terminal-overrides ',xterm-256color:RGB' # so that escapes register immidiately in vim set -sg escape-time 1 set -g focus-events on # mouse support set -g mouse on # change prefix to C-a set -g prefix C-a bind C-a send-prefix unbind C-b # extend scrollback set-option -g history-limit 5000 # vim-like pane resizing bind -r C-k resize-pane -U bind -r C-j resize-pane -D bind -r C-h resize-pane -L bind -r C-l resize-pane -R # vim-like pane switching bind -r k select-pane -U bind -r j select-pane -D bind -r h select-pane -L bind -r l select-pane -R # styling set -g status-style fg=white,bg=default set -g status-left "" set -g status-right "" set -g status-justify centre set -g status-position bottom set -g pane-active-border-style bg=default,fg=default set -g pane-border-style fg=default set -g window-status-current-format "#[fg=cyan]#[fg=black]#[bg=cyan]#I #[bg=brightblack]#[fg=white] #W#[fg=brightblack]#[bg=default] #[bg=default] #[fg=magenta]#[fg=black]#[bg=magenta]λ #[fg=white]#[bg=brightblack] %a %d %b #[fg=magenta]%R#[fg=brightblack]#[bg=default]" set -g window-status-format "#[fg=magenta]#[fg=black]#[bg=magenta]#I #[bg=brightblack]#[fg=white] #W#[fg=brightblack]#[bg=default] " ''; }
4.2. Mac.nix
There are mac-specific tweaks I need to do. In the future if I switch to nixOS full-time, then I wuold likely need to remove the mac-specific packages. An easy way to do this is just keep them in a seperate file:
4.2.1. Yabai
Yabai is my tiling WM of choice. As this is an m1 (aarch64-darwin) laptop, I use the the-future branch, which enables the SA addon on m1 machines and monterey support
Now to configure the package via nix
nix
services.yabai = { enable = false; enableScriptingAddition = false; package = pkgs.yabai-m1; config = { window_border = "off"; # window_border_width = 5; # active_window_border_color = "0xff3B4252"; # normal_window_border_color = "0xff2E3440"; focus_follows_mouse = "autoraise"; mouse_follows_focus = "off"; mouse_drop_action = "stack"; window_placement = "second_child"; window_opacity = "off"; window_topmost = "on"; window_shadow = "on"; active_window_opacity = "1.0"; normal_window_opacity = "1.0"; split_ratio = "0.50"; auto_balance = "on"; mouse_modifier = "fn"; mouse_action1 = "move"; mouse_action2 = "resize"; layout = "bsp"; top_padding = 18; bottom_padding = 46; left_padding = 18; right_padding = 18; window_gap = 18; }; };
4.2.2. Spacebar
Spacebar is my bar of choice on macOS. Its lighter than any web-based ubersicht bar, and looks nice
nix
services.spacebar = { enable = true; package = pkgs.spacebar; config = { position = "bottom"; height = 28; title = "on"; spaces = "on"; power = "on"; clock = "off"; right_shell = "off"; padding_left = 20; padding_right = 20; spacing_left = 25; spacing_right = 25; text_font = ''"Menlo:16.0"''; icon_font = ''"Menlo:16.0"''; background_color = "0xff242730"; foreground_color = "0xffbbc2cf"; space_icon_color = "0xff51afef"; power_icon_strip = " "; space_icon_strip = "I II III IV V VI VII VIII IX X"; spaces_for_all_displays = "on"; display_separator = "on"; display_separator_icon = "|"; clock_format = ''"%d/%m/%y %R"''; right_shell_icon = " "; right_shell_command = "whoami"; }; };
4.2.3. SKHD
Skhd is the hotkey daemon for yabai. As yabai is disabled, it makes sense to disable skhd too for the time being
nix
services.skhd = { enable = false; package = pkgs.skhd; skhdConfig = '' ctrl + alt - h : yabai -m window --focus west ctrl + alt - j : yabai -m window --focus south ctrl + alt - k : yabai -m window --focus north ctrl + alt - l : yabai -m window --focus east # Fill space with window ctrl + alt - 0 : yabai -m window --grid 1:1:0:0:1:1 # Move window ctrl + alt - e : yabai -m window --display 1; yabai -m display --focus 1 ctrl + alt - d : yabai -m window --display 2; yabai -m display --focus 2 ctrl + alt - f : yabai -m window --space next; yabai -m space --focus next ctrl + alt - s : yabai -m window --space prev; yabai -m space --focus prev # Close current window ctrl + alt - w : $(yabai -m window $(yabai -m query --windows --window | jq -re ".id") --close) # Rotate tree ctrl + alt - r : yabai -m space --rotate 90 # Open application ctrl + alt - enter : alacritty ctrl + alt - e : emacs ctrl + alt - b : open -a Safari ctrl + alt - t : yabai -m window --toggle float;\ yabai -m window --grid 4:4:1:1:2:2 ctrl + alt - p : yabai -m window --toggle sticky;\ yabai -m window --toggle topmost;\ yabai -m window --toggle pip ''; };
4.2.4. Homebrew
GUI apps with Nix are finicky at best. As much as I would like to fully give up homebrew, its very annoying having to re-install GUI apps on new systems
nix
homebrew = { brewPrefix = "/opt/homebrew/bin"; enable = true; autoUpdate = true; cleanup = "zap"; # keep it clean global = { brewfile = true; noLock = true; }; taps = [ "homebrew/core" # core "homebrew/cask" # we're using this for casks, after all "homebrew/cask-versions" # needed for firefox-nightly and discord-canary ]; casks = [ "firefox-nightly" # my browser of choice "discord-canary" # chat client of choice "nvidia-geforce-now" # game streaming "via" # keyboard config "hammerspoon" # "wm" "blender" # blender ]; };
4.2.5. Hammerspoon
Yabai breaks very, very often and amethyst just isn’t my cup of tea. Lets use hammerspoon to configure some of our system and implement our own tiling wm. I’ve implemented this elsewhere. We also need to build the spaces dependency from source, since this is an arm64 machine
nix
system.activationScripts.postUserActivation.text = '' sudo cp -r ~/nix-darwin-dotfiles/configs/hammerspoon/ ~/.hammerspoon git clone --depth 1 https://github.com/asmagill/hs._asm.undocumented.spaces.git spaces && cd spaces && make install && cd .. && rm -f -r spaces '';
4.2.6. MacOS Settings
I like my hostname to be the same as the flake’s target
Along with that, lets
- Increase key repeat rate
- Remap Caps to Esc
- Save screenshots to /tmp
- Autohide the dock and menubar
- Show extensions in Finder (and allow it to “quit”)
- Set macOS to use the dark theme
- Configure Trackpad and mouse behavior
- Enable subpixel antialiasing on internal/external displays
nix
system.keyboard = { enableKeyMapping = true; remapCapsLockToEscape = true; }; system.defaults = { screencapture = { location = "/tmp"; }; dock = { autohide = true; showhidden = true; mru-spaces = false; }; finder = { AppleShowAllExtensions = true; QuitMenuItem = true; FXEnableExtensionChangeWarning = true; }; NSGlobalDomain = { AppleInterfaceStyle = "Dark"; AppleKeyboardUIMode = 3; ApplePressAndHoldEnabled = false; AppleFontSmoothing = 1; _HIHideMenuBar = false; InitialKeyRepeat = 10; KeyRepeat = 1; "com.apple.mouse.tapBehavior" = 1; "com.apple.swipescrolldirection" = true; }; }; }
4.3. Pam.nix
Apple’s touchid is an excellent way of authenticating anything quickly and securely. Sadly, sudo doesn’t support it by default, but its an easy fix. T do this, we edit /etc/pam.d/sudo via sed to include the relevent code to enable touchid.
We don’t use environment.etc because this would require that the user manually delete /etc/pam.d/sudo which seems unwise given that applying the nix-darwin configuration requires sudo. We also can’t use system.patches since it only runs once, and so won’t patch in the changes again after OS updates (which remove modifications to this file).
As such, we resort to line addition/deletion in place using sed. We add a comment to the added line that includes the name of the option, to make it easier to identify the line that should be deleted when the option is disabled.
nix
{ config, lib, pkgs, ... }: with lib; let cfg = config.security.pam; mkSudoTouchIdAuthScript = isEnabled: let file = "/etc/pam.d/sudo"; option = "security.pam.enableSudoTouchIdAuth"; in '' ${if isEnabled then '' # Enable sudo Touch ID authentication, if not already enabled if ! grep 'pam_tid.so' ${file} > /dev/null; then sed -i "" '2i\ auth sufficient pam_tid.so # nix-darwin: ${option} ' ${file} fi '' else '' # Disable sudo Touch ID authentication, if added by nix-darwin if grep '${option}' ${file} > /dev/null; then sed -i "" '/${option}/d' ${file} fi ''} ''; in { options = { security.pam.enableSudoTouchIdAuth = mkEnableOption '' Enable sudo authentication with Touch ID When enabled, this option adds the following line to /etc/pam.d/sudo: auth sufficient pam_tid.so (Note that macOS resets this file when doing a system update. As such, sudo authentication with Touch ID won't work after a system update until the nix-darwin configuration is reapplied.) ''; }; config = { system.activationScripts.extraActivation.text = '' # PAM settings echo >&2 "setting up pam..." ${mkSudoTouchIdAuthScript cfg.enableSudoTouchIdAuth} ''; }; }
5. Editors
5.1. Emacs
5.1.1. Note: If you want a proper Emacs Config, look here:
https://tecosaur.github.io/emacs-config/config.html, this is just a compilation of different parts of his (and other’s) configs, as well as a few parts I wrote by my own. I’m slowly working on making my config “mine”
- Credit:
- Tecosaur - For all his help and the excellent config
- Dr. Elken - For his EXWM Module and help on the DOOM Server
- Henrik - For making Doom Emacs in the first place
Includes (snippets) of other software related under the MIT license:
- Doom Emacs Config, 2021 Tecosaur. https://tecosaur.github.io/emacs-config/config.html
- .doom.d, 2021 Elken. https://github.com/elken/.doom.d/blob/master/config.org
Includes (snippets) of other software related under the GPLv3 license:
- .dotfiles, 2021 Daviwil. https://github.com/daviwil/dotfiles
5.1.2. Intro
Customizing an editor can be very rewarding … until you have to leave it. For years I have been looking for ways to avoid this pain. Then I discovered vim-anywhere. The issue is
- I use neovim (and neovide), not vim (and gvim)
- Firenvim is only for browsers
- Even if I found a neovim alternative, you can’t do everything in neovim
I wanted everything, in one place. Hence why I (mostly) switched to Emacs.
Separately, online I have seen the following statement enough times I think it’s a catchphrase
Redditor 1: I just discovered this thing, isn’t it cool.
Redditor 2: Oh, there’s an Emacs mode for that.
This was enough for me to install Emacs, but there are many other reasons to keep using it.
I tried out the spacemacs distribution a bit, but it wasn’t quite to my liking. Then I heard about doom emacs and thought I may as well give that a try.
With Org, I’ve discovered the wonders of literate programming, and with the help of others I’ve switched more and more to just using Emacs (just replace “Linux” with “Emacs” in the comic below).
Thats not to say using Emacs doesn’t have its pitfalls. The performance leaves something to be desired, but the benefits far outweigh the drawbacks. Its unrivaled in extensibility.
Editor | Extensibility | Ecosystem | Ease of Use | Comfort | Completion | Performance |
---|---|---|---|---|---|---|
IDLE | 1 | 1 | 3 | 1 | 1 | 2 |
VSCode | 3 | 3 | 4 | 3.5 | 4 | 3 |
Emacs | 5 | 4 | 2 | 4 | 3.5 | 3 |
(Neo)Vim | 4 | 3 | 2.5 | 3.5 | 4 | 5 |
- Why Emacs?
Emacs is not a text editor, this is a common misnomer. It is far more apt to describe Emacs as a Lisp machine providing a generic user-centric text manipulation environment. That’s quite a mouthful. In simpler terms one can think of Emacs as a platform for text-related applications. It’s a vague and generic definition because Emacs itself is generic.
Good with text. How far does that go? A lot further than one initially thinks:
- Task planning
- File management
- Terminal emulation
- Email client
- Remote server tool
- Git frontend
- Web client/server
- and more…
Ideally, one may use Emacs as the interface to perform input → transform → output cycles, i.e. form a bridge between the human mind and information manipulation.
- The enveloping editor
Emacs allows one to do more in one place than any other application. Why is this good?
- Enables one to complete tasks with a consistent, standard set of keybindings, GUI and editing methods — learn once, use everywhere
- Reduced context-switching
- Compressing the stages of a project — a more centralised workflow can progress with greater ease
- Integration between tasks previously relegated to different applications, but with a common subject — e.g. linking to an email in a to-do list
Emacs can be thought of as a platform within which various elements of your workflow may settle, with the potential for rich integrations between them — a life IDE if you will.
Today, many aspects of daily computer usage are split between different applications which act like islands, but this often doesn’t mirror how we actually use our computers. Emacs, if one goes down the rabbit hole, can give users the power to bridge this gap.
- Notes for the unwary adventurer
The lovely
doom doctor
is good at diagnosing most missing things, but here are a few extras.My nix config should handle installing all these dependencies, If you aren’t using it, here is the list of packages you may need:
nix
environment.systemPackages = with pkgs; [ # Emacs deps ((emacsPackagesNgGen emacsGcc).emacsWithPackages (epkgs: [ epkgs.vterm epkgs.pdf-tools ])) ## make sure ripgrep supports pcre2 (for vertico) (ripgrep.override { withPCRE2 = true; }) sqlite zstd ## Required for plots but not installed by default gnuplot ## Required for dictionaries but not installed by default sdcv # aspell and \latex (aspellWithDicts (ds: with ds; [ en en-computers en-science ])) (texlive.combine { inherit (texlive) scheme-small dvipng dvisvgm l3packages xcolor soul adjustbox collectbox amsmath siunitx cancel mathalpha capt-of chemfig wrapfig mhchem fvextra cleveref latexmk tcolorbox environ arev amsfonts simplekv alegreya sourcecodepro newpx svg catchfile transparent hanging; }) # Language deps tree-sitter python39Packages.grip python39Packages.pyflakes python39Packages.isort python39Packages.pytest nodePackages.pyright pipenv nixfmt black rust-analyzer rust-bin.nightly.latest.default shellcheck # jdk # Terminal utils and rust alternatives :tm: fd ]; fonts = { enableFontDir = true; fonts = with pkgs; [ overpass alegreya alegreya-sans emacs-all-the-icons-fonts sf-mono-liga-bin ]; };
- A LaTeX Compiler is required for the mathematics rendering performed in org, and that wonderful pdf/html export we have going. I recommend Tectonic if you are new, this config uses XeLaTeX.
- I use the Overpass font as a go-to sans serif.
It’s used as my
doom-variable-pitch-font
I have chosen it because it possesses a few characteristics I consider desirable, namely:- A clean, and legible style. Highway-style fonts tend to be designed to be clear at a glance, and work well with a thicker weight, and this is inspired by Highway Gothic.
- It’s slightly quirky. Look at the diagonal cut on stems for example. Helvetica is a masterful design, but I like a bit more pizzazz now and then.
- Note: Alegreya is used for my latex export and writeroom mode configurations
- I use my patched SFMono font as a go-to monospace.
I have chosen it because it possesses a few characteristics I consider
desirable, namely:
- Elegent characters, and good ligatures/unicode support
- It fits will with the rest of my system
- A few LSP servers. Take a look at init.el’ to see which modules have the
+lsp
flag. - Gnuplot, used for org-plot.
- A build of emacs with modules and xwidgets support. I also recommend the native-comp flag with emacs28.
5.1.3. Doom Configuration
- Modules
Doom has this lovely modular configuration base that takes a lot of work out of configuring Emacs. Each module (when enabled) can provide a list of packages to install (on
doom sync
) and configuration to be applied. The modules can also have flags applied to tweak their behaviour.init.elEmacs Lisp
;;; init.el -*- lexical-binding: t; -*- ;; This file controls what Doom modules are enabled and what order they load in. ;; Press 'K' on a module to view its documentation, and 'gd' to browse its directory. (doom! :completion <<doom-completion>> :ui <<doom-ui>> :editor <<doom-editor>> :emacs <<doom-emacs>> :term <<doom-term>> :checkers <<doom-checkers>> :tools <<doom-tools>> :os <<doom-os>> :lang <<doom-lang>> :email <<doom-email>> :app <<doom-app>> :config <<doom-config>>)
- Structure
As you may have noticed by this point, this is a literate configuration. Doom has good support for this which we access though the
literate
module.While we’re in the
:config
section, we’ll use Dooms nicer defaults, along with the bindings and smartparens behaviour (the flags aren’t documented, but they exist).- Asynchronous config tangling
Doom adds an org-mode hook
+literate-enable-recompile-h
. This is a nice idea, but it’s too blocking for my taste. Since I trust my tangling to be fairly straightforward, I’ll just redefine it to a simpler, async, function.Emacs Lisp
(defvar +literate-tangle--proc nil) (defvar +literate-tangle--proc-start-time nil) (defadvice! +literate-tangle-async-h () "A very simplified version of `+literate-tangle-h', but async." :override #'+literate-tangle-h (unless (getenv "__NOTANGLE") (let ((default-directory doom-private-dir)) (when +literate-tangle--proc (message "Killing outdated tangle process...") (set-process-sentinel +literate-tangle--proc #'ignore) (kill-process +literate-tangle--proc) (sit-for 0.3)) ; ensure the message is seen for a bit (setq +literate-tangle--proc-start-time (float-time) +literate-tangle--proc (start-process "tangle-config" (get-buffer-create " *tangle config*") "emacs" "--batch" "--eval" (format "(progn \ (require 'ox) \ (require 'ob-tangle) \ (setq org-confirm-babel-evaluate nil \ org-inhibit-startup t \ org-mode-hook nil \ write-file-functions nil \ before-save-hook nil \ after-save-hook nil \ vc-handled-backends nil \ org-startup-folded nil \ org-startup-indented nil) \ (org-babel-tangle-file \"%s\" \"%s\"))" +literate-config-file (expand-file-name (concat doom-module-config-file ".el"))))) (set-process-sentinel +literate-tangle--proc #'+literate-tangle--sentinel) (run-at-time nil nil (lambda () (message "Tangling config.org"))) ; ensure shown after a save message "Tangling config.org..."))) (defun +literate-tangle--sentinel (process signal) (cond ((and (eq 'exit (process-status process)) (= 0 (process-exit-status process))) (message "Tangled config.org sucessfully (took %.1fs)" (- (float-time) +literate-tangle--proc-start-time)) (setq +literate-tangle--proc nil)) ((memq (process-status process) (list 'exit 'signal)) (+popup-buffer (get-buffer " *tangle config*")) (message "Failed to tangle config.org (after %.1fs)" (- (float-time) +literate-tangle--proc-start-time)) (setq +literate-tangle--proc nil)))) (defun +literate-tangle-check-finished () (when (and (process-live-p +literate-tangle--proc) (yes-or-no-p "Config is currently retangling, would you please wait a few seconds?")) (switch-to-buffer " *tangle config*") (signal 'quit nil))) (add-hook! 'kill-emacs-hook #'+literate-tangle-check-finished)
- Asynchronous config tangling
- Interface
There’s a lot that can be done to enhance Emacs’ capabilities. I reckon enabling half the modules Doom provides should do it.
doom-completionEmacs Lisp
(company ; the ultimate code completion backend +childframe) ; ... when your children are better than you ;;helm ; the *other* search engine for love and life ;;ido ; the other *other* search engine... ;;(ivy ; a search engine for love and life ;; +icons ; ... icons are nice ;; +prescient) ; ... I know what I want(ed) (vertico +icons) ; the search engine of the future
doom-uiEmacs Lisp
;;deft ; notational velocity for Emacs doom ; what makes DOOM look the way it does doom-dashboard ; a nifty splash screen for Emacs doom-quit ; DOOM quit-message prompts when you quit Emacs ;;(emoji +unicode) ; 🙂 ;;fill-column ; a `fill-column' indicator hl-todo ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW ;;hydra ; quick documentation for related commands ;;indent-guides ; highlighted indent columns, notoriously slow (ligatures ; ligatures and symbols to make your code pretty again +extra) ; for those who dislike letters minimap ; show a map of the code on the side modeline ; snazzy, Atom-inspired modeline, plus API ;; +light) ; the doom modeline is a bit much, the default is a bit little nav-flash ; blink the current line after jumping ;;neotree ; a project drawer, like NERDTree for vim ophints ; highlight the region an operation acts on (popup ; tame sudden yet inevitable temporary windows +all ; catch all popups that start with an asterix +defaults) ; default popup rules ;;(tabs ; an tab bar for Emacs ;; +centaur-tabs) ; ... with prettier tabs treemacs ; a project drawer, like neotree but cooler ;;unicode ; extended unicode support for various languages vc-gutter ; vcs diff in the fringe vi-tilde-fringe ; fringe tildes to mark beyond EOB ;;(window-select +numbers) ; visually switch windows workspaces ; tab emulation, persistence & separate workspaces zen ; distraction-free coding or writing
doom-editorEmacs Lisp
(evil +everywhere) ; come to the dark side, we have cookies file-templates ; auto-snippets for empty files fold ; (nigh) universal code folding (format +onsave) ; automated prettiness ;;god ; run Emacs commands without modifier keys ;;lispy ; vim for lisp, for people who don't like vim ;;multiple-cursors ; editing in many places at once ;;objed ; text object editing for the innocent ;;parinfer ; turn lisp into python, sort of ;;rotate-text ; cycle region at point between text candidates snippets ; my elves. They type so I don't have to ;;word-wrap ; soft wrapping with language-aware indent
doom-emacsEmacs Lisp
(dired +icons) ; making dired pretty [functional] electric ; smarter, keyword-based electric-indent (ibuffer +icons) ; interactive buffer management undo ; persistent, smarter undo for your inevitable mistakes vc ; version-control and Emacs, sitting in a tree
doom-termEmacs Lisp
;;eshell ; the elisp shell that works everywhere ;;shell ; simple shell REPL for Emacs ;;term ; basic terminal emulator for Emacs vterm ; the best terminal emulation in Emacs
doom-checkersEmacs Lisp
syntax ; tasing you for every semicolon you forget (:if (executable-find "aspell") spell) ; tasing you for misspelling mispelling (:if (executable-find "languagetool") grammar) ; tasing grammar mistake every you make
doom-toolsEmacs Lisp
;;ansible ; a crucible for infrastructure as code ;;biblio ; Writes a PhD for you (citation needed) (debugger +lsp) ; FIXME stepping through code, to help you add bugs ;;direnv ; be direct about your environment ;;docker ; port everything to containers ;;editorconfig ; let someone else argue about tabs vs spaces ;;ein ; tame Jupyter notebooks with emacs (eval +overlay) ; run code, run (also, repls) ;;gist ; interacting with github gists (lookup ; helps you navigate your code and documentation +dictionary ; dictionary/thesaurus is nice +docsets) ; ...or in Dash docsets locally lsp ; Language Server Protocol (magit ; a git porcelain for Emacs +forge) ; interface with git forges ;;make ; run make tasks from Emacs ;;pass ; password manager for nerds pdf ; pdf enhancements ;;prodigy ; FIXME managing external services & code builders ;;rgb ; creating color strings ;;taskrunner ; taskrunner for all your projects ;;terraform ; infrastructure as code ;;tmux ; an API for interacting with tmux ;;tree-sitter ; ... sitting in a tree ;;upload ; map local to remote projects via ssh/ftp
doom-osEmacs Lisp
(:if IS-MAC macos) ; improve compatibility with macOS ;;tty ; improve the terminal Emacs experience
- Language support
We can be rather liberal with enabling support for languages as the associated packages/configuration are (usually) only loaded when first opening an associated file.
doom-langEmacs Lisp
;;agda ; types of types of types of types... ;;beancount ; mind the GAAP (cc +lsp) ; C/C++/Obj-C madness ;;clojure ; java with a lisp ;;common-lisp ; if you've seen one lisp, you've seen them all ;;coq ; proofs-as-programs ;;crystal ; ruby at the speed of c ;;csharp ; unity, .NET, and mono shenanigans ;;data ; config/data formats ;;(dart +flutter) ; paint ui and not much else ;;dhall ; JSON with FP sprinkles ;;elixir ; erlang done right ;;elm ; care for a cup of TEA? emacs-lisp ; drown in parentheses ;;erlang ; an elegant language for a more civilized age ;;ess ; emacs speaks statistics ;;faust ; dsp, but you get to keep your soul ;;fsharp ; ML stands for Microsoft's Language ;;fstar ; (dependent) types and (monadic) effects and Z3 ;;gdscript ; the language you waited for ;;(go +lsp) ; the hipster dialect ;;(haskell +lsp) ; a language that's lazier than I am ;;hy ; readability of scheme w/ speed of python ;;idris ; ;;json ; At least it ain't XML ;;(java +lsp) ; the poster child for carpal tunnel syndrome ;;(javascript +lsp) ; all(hope(abandon(ye(who(enter(here)))))) ;;(julia +lsp) ; Python, R, and MATLAB in a blender ;;(kotlin +lsp) ; a better, slicker Java(Script) (latex ; writing papers in Emacs has never been so fun +fold ; fold the clutter away nicities +latexmk ; modern latex plz +cdlatex ; quick maths symbols +lsp) ;;lean ; proof that mathematicians need help ;;factor ; for when scripts are stacked against you ;;ledger ; an accounting system in Emacs (lua ; one-based indices? one-based indices +lsp) (markdown +grip) ; writing docs for people to ignore ;;nim ; python + lisp at the speed of c nix ; I hereby declare "nix geht mehr!" ;;ocaml ; an objective camel (org ; organize your plain life in plain text +pretty ; yessss my pretties! (nice unicode symbols) +dragndrop ; drag & drop files/images into org buffers ;;+hugo ; use Emacs for hugo blogging ;;+noter ; enhanced PDF notetaking +jupyter ; ipython/jupyter support for babel +pandoc ; export-with-pandoc support +gnuplot ; who doesn't like pretty pictures +pomodoro ; be fruitful with the tomato technique +present ; using org-mode for presentations +roam2) ; wander around notes ;;php ; perl's insecure younger brother ;;plantuml ; diagrams for confusing people more ;;purescript ; javascript, but functional (python +lsp +pyright) ; beautiful is better than ugly ;;qt ; the 'cutest' gui framework ever ;;racket ; a DSL for DSLs ;;raku ; the artist formerly known as perl6 ;;rest ; Emacs as a REST client ;;rst ; ReST in peace ;;(ruby +rails) ; 1.step {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"} (rust +lsp) ; Fe2O3.unwrap().unwrap().unwrap().unwrap() ;;scala ; java, but good ;;scheme ; a fully conniving family of lisps sh ; she sells {ba,z,fi}sh shells on the C xor ;;sml ; no, the /other/ ML ;;solidity ; do you need a blockchain? No. ;;swift ; who asked for emoji variables? ;;terra ; Earth and Moon in alignment for performance. ;;web ; the tubes ;;yaml ; JSON, but readable ;;zig ; C, but simpler
- Everything in Emacs
It’s just too convenient being able to have everything in Emacs. I couldn’t resist the Email and Feed modules.
doom-emailEmacs Lisp
(:if (executable-find "mu") (mu4e +org +gmail)) ;;notmuch ;;(wanderlust +gmail)
doom-appEmacs Lisp
;;calendar ; A dated approach to timetabling ;;emms ; Multimedia in Emacs is music to my ears ;;everywhere ; *leave* Emacs!? You must be joking. ;;irc ; how neckbeards socialize ;;(rss +org) ; emacs as an RSS reader ;;twitter ; twitter client https://twitter.com/vnought
- Structure
- Packages
Unlike most literate configurations I
am lazylike to keep all my packages in one placepackages.elEmacs Lisp
;; -*- no-byte-compile: t; -*- ;;; $DOOMDIR/packages.el ;;org <<org>> ;;latex <<latex>> ;;markdown and html <<web>> ;;looks <<looks>> ;;emacs additions <<emacs>> ;;lsp <<lsp>> ;;fun <<fun>>
- Org:
The majority of my work in emacs is done in org mode, even this configuration was written in org! It makes sense that the majority of my packages are for tweaking org then
orgEmacs Lisp
(package! doct) (package! citar) (package! citeproc) (package! org-appear) (package! org-roam-ui) (package! org-cite-csl-activate :recipe (:host github :repo "andras-simonyi/org-cite-csl-activate")) (package! org-pandoc-import ;https://github.com/melpa/melpa/pull/7326 :recipe (:host github :repo "tecosaur/org-pandoc-import" :files ("*.el" "filters" "preprocessors")))
- \LaTeX:
When I’m not working in org, I’m probably exporting it to latex. Lets adjust that a bit too
- Web:
Sometimes I need to use markdown too. Note: emacs-webkit is temporarily disabled because of its refusal to work without requiring org
webEmacs Lisp
(package! ox-gfm) (package! websocket) ;;(package! webkit ;; :recipe (:host github ;; :repo "akirakyle/emacs-webkit" ;; :branch "main" ;; :files (:defaults "*.js" "*.css" "*.so" "*.nix") ;; :pre-build (("nix-shell" "shell.nix" "--command make"))))
- Looks:
Making emacs look good is first priority, actually working in it is second
looksEmacs Lisp
(unpin! doom-themes) (unpin! doom-modeline) (package! modus-themes) (package! solaire-mode :disable t) (package! ox-chameleon :recipe (:host github :repo "tecosaur/ox-chameleon")) ;soon :tm:
- Emacs Tweaks:
Emacs is missing just a few packages that I need to make it my OS. Specifically, screenshot capabilities are nice, and using the same dictionaries accross
operating systemsbootloaders would be nice too!emacsEmacs Lisp
(package! lexic) (package! pdf-tools) (package! magit-delta) (package! screenshot :recipe (:host github :repo "Jimmysit0/screenshot")) ;https://github.com/melpa/melpa/pull/7327
- LSP:
I like to live life on the edge
- Fun:
We do a little trolling (and reading)
funEmacs Lisp
(package! nov) (package! xkcd) (package! keycast) (package! selectric-mode :recipe (:local-repo "lisp/selectric-mode"))
- Org:
5.1.4. Basic Configuration
Make this file run (slightly) faster with lexical binding
- Personal information
Of course we need to tell emacs who I am
- Authinfo
I frequently delete my ~/.emacs.d for fun, so having authinfo in a seperate file sounds like a good idea
elisp
(setq auth-sources '("~/.authinfo.gpg") auth-source-cache-expiry nil) ; default is 7200 (2h)
- Emacsclient
mu4e is a bit finicky with emacsclient, and org takes forever to load. The solution? Use tecosaurs greedy daemon startup
elisp
(defun greedily-do-daemon-setup () (require 'org) (require 'vertico) (require 'consult) (require 'marginalia) (when (require 'mu4e nil t) (setq mu4e-confirm-quit t) (setq +mu4e-lock-greedy t) (setq +mu4e-lock-relaxed t) (+mu4e-lock-add-watcher) (when (+mu4e-lock-available t) (mu4e~start)))) (when (daemonp) (add-hook 'emacs-startup-hook #'greedily-do-daemon-setup) (add-hook 'emacs-startup-hook #'init-mixed-pitch-h))
- Shell
I use the fish shell. If you use zsh/bash, be sure to change this
- Vterm
Vterm is my terminal emulator of choice. We can tell it to use ligatures, and also tell it to compile automatically Vterm clearly wins the terminal war. Also doesn’t need much configuration out of the box, although the shell integration does. You can find that in ~/.config/fish/config.fish
- Always compile
Fixes a weird bug with native-comp
- Kill buffer
If the process exits, kill the vterm buffer
- Functions
- Ligatures
Use ligatures from within vterm (and eshell), we do this by redefining the variable where not to show ligatures. On the other hand, in select modes we want to use extra ligatures, so lets enable that.
- Always compile
- Vterm
- Fonts
I like the apple fonts for programming, so I’ll go with Liga SFMono Nerd Font. I prefer a rounder font for plain text, so I’ll go with Overpass for that. I have a retina display as well, so lets keep the fonts light.
elisp
;;fonts (setq doom-font (font-spec :family "Liga SFMono Nerd Font" :size 14) doom-big-font (font-spec :family "Liga SFMono Nerd Font" :size 20) doom-variable-pitch-font (font-spec :family "Overpass" :size 16) doom-unicode-font (font-spec :family "Liga SFMono Nerd Font") doom-serif-font (font-spec :family "Liga SFMono Nerd Font" :weight 'light))
For mixed pitch, I would go with something comfier. I like Alegreya Sans for a minimalist feel, so lets go with that
elisp
;;mixed pitch modes (defvar mixed-pitch-modes '(org-mode LaTeX-mode markdown-mode gfm-mode Info-mode) "Modes that `mixed-pitch-mode' should be enabled in, but only after UI initialisation.") (defun init-mixed-pitch-h () "Hook `mixed-pitch-mode' into each mode in `mixed-pitch-modes'. Also immediately enables `mixed-pitch-modes' if currently in one of the modes." (when (memq major-mode mixed-pitch-modes) (mixed-pitch-mode 1)) (dolist (hook mixed-pitch-modes) (add-hook (intern (concat (symbol-name hook) "-hook")) #'mixed-pitch-mode))) (add-hook 'doom-init-ui-hook #'init-mixed-pitch-h) (add-hook! 'org-mode-hook #'+org-pretty-mode) ;enter mixed pitch mode in org mode ;;set mixed pitch font (after! mixed-pitch (defface variable-pitch-serif '((t (:family "serif"))) "A variable-pitch face with serifs." :group 'basic-faces) (setq mixed-pitch-set-height t) (setq variable-pitch-serif-font (font-spec :family "Alegreya Sans" :size 16 :weight 'Medium)) (set-face-attribute 'variable-pitch-serif nil :font variable-pitch-serif-font) (defun mixed-pitch-serif-mode (&optional arg) "Change the default face of the current buffer to a serifed variable pitch, while keeping some faces fixed pitch." (interactive) (let ((mixed-pitch-face 'variable-pitch-serif)) (mixed-pitch-mode (or arg 'toggle)))))
Harfbuzz is missing the beautiful ff ffi ffj ffl fft fi fj ft Th ligatures, lets add those back in with the help of composition-function-table
elisp
(set-char-table-range composition-function-table ?f '(["\\(?:ff?[fijlt]\\)" 0 font-shape-gstring])) (set-char-table-range composition-function-table ?T '(["\\(?:Th\\)" 0 font-shape-gstring]))
- Font collections
Using the lovely conditional preamble, I’ll define a number of font collections that can be used for LaTeX exports. Who knows, maybe I’ll use it with other export formats too at some point.
To start with I’ll create a default state variable and register fontset as part of #+options.
Emacs Lisp
(after! ox-latex (defvar org-latex-default-fontset 'alegreya "Fontset from `org-latex-fontsets' to use by default. As cm (computer modern) is TeX's default, that causes nothing to be added to the document. If \"nil\" no custom fonts will ever be used.") (eval '(cl-pushnew '(:latex-font-set nil "fontset" org-latex-default-fontset) (org-export-backend-options (org-export-get-backend 'latex)))))
Then a function is needed to generate a LaTeX snippet which applies the fontset. It would be nice if this could be done for individual styles and use different styles as the main document font. If the individual typefaces for a fontset are defined individually as
:serif
,:sans
,:mono
, and:maths
. I can use those to generate LaTeX for subsets of the full fontset. Then, if I don’t let any fontset names have - in them, I can use -sans and -mono as suffixes that specify the document font to use.Emacs Lisp
(after! ox-latex (defun org-latex-fontset-entry () "Get the fontset spec of the current file. Has format \"name\" or \"name-style\" where 'name' is one of the cars in `org-latex-fontsets'." (let ((fontset-spec (symbol-name (or (car (delq nil (mapcar (lambda (opt-line) (plist-get (org-export--parse-option-keyword opt-line 'latex) :latex-font-set)) (cdar (org-collect-keywords '("OPTIONS")))))) org-latex-default-fontset)))) (cons (intern (car (split-string fontset-spec "-"))) (when (cadr (split-string fontset-spec "-")) (intern (concat ":" (cadr (split-string fontset-spec "-")))))))) (defun org-latex-fontset (&rest desired-styles) "Generate a LaTeX preamble snippet which applies the current fontset for DESIRED-STYLES." (let* ((fontset-spec (org-latex-fontset-entry)) (fontset (alist-get (car fontset-spec) org-latex-fontsets))) (if fontset (concat (mapconcat (lambda (style) (when (plist-get fontset style) (concat (plist-get fontset style) "\n"))) desired-styles "") (when (memq (cdr fontset-spec) desired-styles) (pcase (cdr fontset-spec) (:serif "\\renewcommand{\\familydefault}{\\rmdefault}\n") (:sans "\\renewcommand{\\familydefault}{\\sfdefault}\n") (:mono "\\renewcommand{\\familydefault}{\\ttdefault}\n")))) (error "Font-set %s is not provided in org-latex-fontsets" (car fontset-spec))))))
Now that all the functionality has been implemented, we should hook it into our preamble generation.
Emacs Lisp
(after! ox-latex (add-to-list 'org-latex-conditional-features '(org-latex-default-fontset . custom-font) t) (add-to-list 'org-latex-feature-implementations '(custom-font :snippet (org-latex-fontset :serif :sans :mono) :order 0) t) (add-to-list 'org-latex-feature-implementations '(.custom-maths-font :eager t :when (custom-font maths) :snippet (org-latex-fontset :maths) :order 0.3) t))
Finally, we just need to add some fonts.
Emacs Lisp
(after! ox-latex (defvar org-latex-fontsets '((cm nil) ; computer modern (## nil) ; no font set (alegreya :serif "\\usepackage[osf]{Alegreya}" :sans "\\usepackage{AlegreyaSans}" :mono "\\usepackage[scale=0.88]{sourcecodepro}" :maths "\\usepackage[varbb]{newpxmath}") (biolinum :serif "\\usepackage[osf]{libertineRoman}" :sans "\\usepackage[sfdefault,osf]{biolinum}" :mono "\\usepackage[scale=0.88]{sourcecodepro}" :maths "\\usepackage[libertine,varvw]{newtxmath}") (fira :sans "\\usepackage[sfdefault,scale=0.85]{FiraSans}" :mono "\\usepackage[scale=0.80]{FiraMono}" :maths "\\usepackage{newtxsf} % change to firamath in future?") (kp :serif "\\usepackage{kpfonts}") (newpx :serif "\\usepackage{newpxtext}" :sans "\\usepackage{gillius}" :mono "\\usepackage[scale=0.9]{sourcecodepro}" :maths "\\usepackage[varbb]{newpxmath}") (noto :serif "\\usepackage[osf]{noto-serif}" :sans "\\usepackage[osf]{noto-sans}" :mono "\\usepackage[scale=0.96]{noto-mono}" :maths "\\usepackage{notomath}") (plex :serif "\\usepackage{plex-serif}" :sans "\\usepackage{plex-sans}" :mono "\\usepackage[scale=0.95]{plex-mono}" :maths "\\usepackage{newtxmath}") ; may be plex-based in future (source :serif "\\usepackage[osf]{sourceserifpro}" :sans "\\usepackage[osf]{sourcesanspro}" :mono "\\usepackage[scale=0.95]{sourcecodepro}" :maths "\\usepackage{newtxmath}") ; may be sourceserifpro-based in future (times :serif "\\usepackage{newtxtext}" :maths "\\usepackage{newtxmath}")) "Alist of fontset specifications. Each car is the name of the fontset (which cannot include \"-\"). Each cdr is a plist with (optional) keys :serif, :sans, :mono, and :maths. A key's value is a LaTeX snippet which loads such a font."))
When we’re using Alegreya we can apply a lovely little tweak to tabular which (locally) changes the figures used to lining fixed-width.
Emacs Lisp
(after! ox-latex (add-to-list 'org-latex-conditional-features '((string= (car (org-latex-fontset-entry)) "alegreya") . alegreya-typeface)) (add-to-list 'org-latex-feature-implementations '(alegreya-typeface) t) (add-to-list 'org-latex-feature-implementations'(.alegreya-tabular-figures :eager t :when (alegreya-typeface table) :order 0.5 :snippet " \\makeatletter % tabular lining figures in tables \\renewcommand{\\tabular}{\\AlegreyaTLF\\let\\@halignto\\@empty\\@tabular} \\makeatother\n") t))
Due to the Alegreya’s metrics, the \LaTeX symbol doesn’t quite look right. We can correct for this by redefining it with subtlety shifted kerning.
Emacs Lisp
(after! ox-latex (add-to-list 'org-latex-conditional-features '("LaTeX" . latex-symbol)) (add-to-list 'org-latex-feature-implementations '(latex-symbol :when alegreya-typeface :order 0.5 :snippet " \\makeatletter % Kerning around the A needs adjusting \\DeclareRobustCommand{\\LaTeX}{L\\kern-.24em% {\\sbox\\z@ T% \\vbox to\\ht\\z@{\\hbox{\\check@mathfonts \\fontsize\\sf@size\\z@ \\math@fontsfalse\\selectfont A}% \\vss}% }% \\kern-.10em% \\TeX} \\makeatother\n") t))
Just in case the fonts aren’t there, lets add check to notify the user of the issue. Seems like I forget ot install fonts every time I switch between
distrosemacs bootloadersdetect-missing-fontselisp
;; (defvar required-fonts '("Overpass" "Liga SFMono Nerd Font" "Alegreya" )) ;; (defvar available-fonts ;; (delete-dups (or (font-family-list) ;; (split-string (shell-command-to-string "fc-list : family") ;; "[,\n]")))) ;; (defvar missing-fonts ;; (delq nil (mapcar ;; (lambda (font) ;; (unless (delq nil (mapcar (lambda (f) ;; (string-match-p (format "^%s$" font) f)) ;; available-fonts)) ;; font)) ;; required-fonts))) ;; (if missing-fonts ;; (pp-to-string ;; `(unless noninteractive ;; (add-hook! 'doom-init-ui-hook ;; (run-at-time nil nil ;; (lambda () ;; (message "%s missing the following fonts: %s" ;; (propertize "Warning!" 'face '(bold warning)) ;; (mapconcat (lambda (font) ;; (propertize font 'face 'font-lock-variable-name-face)) ;; ',missing-fonts ;; ", ")) ;; (sleep-for 0.5)))))) ;; ";; No missing fonts detected")
- Font collections
- Themes
Right now I’m using nord, but I use doom-vibrant sometimes
elisp
(setq doom-theme 'modus-vivendi) (setq doom-fw-padded-modeline t) (setq doom-one-light-padded-modeline t) (setq doom-nord-padded-modeline t) (setq doom-vibrant-padded-modeline t)
- Modus Themes
Generally I use doom-themes, but I also like the new Modus-themes bundled with emacs28/29
elisp
(use-package modus-themes :init ;; Add all your customizations prior to loading the themes (setq modus-themes-italic-constructs t modus-themes-completions 'opinionated modus-themes-variable-pitch-headings t modus-themes-scale-headings t modus-themes-variable-pitch-ui nil modus-themes-org-agenda '((header-block . (variable-pitch scale-title)) (header-date . (grayscale bold-all))) modus-themes-org-blocks '(grayscale) modus-themes-mode-line '(borderless) modus-themes-region '(bg-only no-extend)) ;; Load the theme files before enabling a theme (modus-themes-load-themes) :config (modus-themes-load-vivendi) :bind ("<f5>" . modus-themes-toggle))
- Modus Themes
- Company
I think company is a bit too quick to recommend some stuff
elisp
(after! company (setq company-idle-delay 0.1 company-minimum-prefix-length 1 company-selection-wrap-around t company-require-match 'never company-dabbrev-downcase nil company-dabbrev-ignore-case t company-dabbrev-other-buffers nil company-tooltip-limit 5 company-tooltip-minimum-width 50)) (set-company-backend! '(text-mode markdown-mode gfm-mode) '(:seperate company-yasnippet company-files)) ;;nested snippets (setq yas-triggers-in-field t)
Lets add some snippets for latex
elisp
(use-package! aas :commands aas-mode) (use-package! laas :hook (LaTeX-mode . laas-mode) :config (defun laas-tex-fold-maybe () (unless (equal "/" aas-transient-snippet-key) (+latex-fold-last-macro-a))) (add-hook 'org-mode #'laas-mode) (add-hook 'aas-post-snippet-expand-hook #'laas-tex-fold-maybe))
And with a little help from henrik, lets use those snippets in org mode
elisp
(defadvice! fixed-org-yas-expand-maybe-h () "Expand a yasnippet snippet, if trigger exists at point or region is active. Made for `org-tab-first-hook'." :override #'+org-yas-expand-maybe-h (when (and (featurep! :editor snippets) (require 'yasnippet nil t) (bound-and-true-p yas-minor-mode)) (and (let ((major-mode (cond ((org-in-src-block-p t) (org-src-get-lang-mode (org-eldoc-get-src-lang))) ((org-inside-LaTeX-fragment-p) 'latex-mode) (major-mode))) (org-src-tab-acts-natively nil) ; causes breakages ;; Smart indentation doesn't work with yasnippet, and painfully slow ;; in the few cases where it does. (yas-indent-line 'fixed)) (cond ((and (or (not (bound-and-true-p evil-local-mode)) (evil-insert-state-p) (evil-emacs-state-p)) (or (and (bound-and-true-p yas--tables) (gethash major-mode yas--tables)) (progn (yas-reload-all) t)) (yas--templates-for-key-at-point)) (yas-expand) t) ((use-region-p) (yas-insert-snippet) t))) ;; HACK Yasnippet breaks org-superstar-mode because yasnippets is ;; overzealous about cleaning up overlays. (when (bound-and-true-p org-superstar-mode) (org-superstar-restart)))))
Source code blocks are a pain in org-mode, so lets make a few functions to help with our snippets
Emacs Lisp
(defun +yas/org-src-header-p () "Determine whether `point' is within a src-block header or header-args." (pcase (org-element-type (org-element-context)) ('src-block (< (point) ; before code part of the src-block (save-excursion (goto-char (org-element-property :begin (org-element-context))) (forward-line 1) (point)))) ('inline-src-block (< (point) ; before code part of the inline-src-block (save-excursion (goto-char (org-element-property :begin (org-element-context))) (search-forward "]{") (point)))) ('keyword (string-match-p "^header-args" (org-element-property :value (org-element-context))))))
Now let’s write a function we can reference in yasnippets to produce a nice interactive way to specify header args.
Emacs Lisp
(defun +yas/org-prompt-header-arg (arg question values) "Prompt the user to set ARG header property to one of VALUES with QUESTION. The default value is identified and indicated. If either default is selected, or no selection is made: nil is returned." (let* ((src-block-p (not (looking-back "^#\\+property:[ \t]+header-args:.*" (line-beginning-position)))) (default (or (cdr (assoc arg (if src-block-p (nth 2 (org-babel-get-src-block-info t)) (org-babel-merge-params org-babel-default-header-args (let ((lang-headers (intern (concat "org-babel-default-header-args:" (+yas/org-src-lang))))) (when (boundp lang-headers) (eval lang-headers t))))))) "")) default-value) (setq values (mapcar (lambda (value) (if (string-match-p (regexp-quote value) default) (setq default-value (concat value " " (propertize "(default)" 'face 'font-lock-doc-face))) value)) values)) (let ((selection (consult--read question values :default default-value))) (unless (or (string-match-p "(default)$" selection) (string= "" selection)) selection))))
Finally, we fetch the language information for new source blocks.
Since we’re getting this info, we might as well go a step further and also provide the ability to determine the most popular language in the buffer that doesn’t have any header-args set for it (with #+properties).
Emacs Lisp
(defun +yas/org-src-lang () "Try to find the current language of the src/header at `point'. Return nil otherwise." (let ((context (org-element-context))) (pcase (org-element-type context) ('src-block (org-element-property :language context)) ('inline-src-block (org-element-property :language context)) ('keyword (when (string-match "^header-args:\\([^ ]+\\)" (org-element-property :value context)) (match-string 1 (org-element-property :value context))))))) (defun +yas/org-last-src-lang () "Return the language of the last src-block, if it exists." (save-excursion (beginning-of-line) (when (re-search-backward "^[ \t]*#\\+begin_src" nil t) (org-element-property :language (org-element-context))))) (defun +yas/org-most-common-no-property-lang () "Find the lang with the most source blocks that has no global header-args, else nil." (let (src-langs header-langs) (save-excursion (goto-char (point-min)) (while (re-search-forward "^[ \t]*#\\+begin_src" nil t) (push (+yas/org-src-lang) src-langs)) (goto-char (point-min)) (while (re-search-forward "^[ \t]*#\\+property: +header-args" nil t) (push (+yas/org-src-lang) header-langs))) (setq src-langs (mapcar #'car ;; sort alist by frequency (desc.) (sort ;; generate alist with form (value . frequency) (cl-loop for (n . m) in (seq-group-by #'identity src-langs) collect (cons n (length m))) (lambda (a b) (> (cdr a) (cdr b)))))) (car (cl-set-difference src-langs header-langs :test #'string=))))
Lets also include << to autocomplete, as with () and {}
And lastly lets add some helpful snippets for org-mode, and add a better templete
- LSP
I think the LSP is a bit intrusive (especially with inline suggestions), so lets make it behave a bit more
elisp
(use-package! lsp-ui :hook (lsp-mode . lsp-ui-mode) :config (setq lsp-ui-sideline-enable nil; not anymore useful than flycheck lsp-lens-enable t lsp-ui-doc-enable t lsp-tex-server 'digestif lsp-headerline-breadcrumb-enable nil lsp-ui-peek-enable t lsp-ui-peek-fontify 'on-demand lsp-enable-symbol-highlighting nil))
The rust language server also has some extra features I would like to enable
elisp
(after! lsp-rust (setq lsp-rust-server 'rust-analyzer lsp-rust-analyzer-display-chaining-hints t lsp-rust-analyzer-display-parameter-hints t lsp-rust-analyzer-server-display-inlay-hints t lsp-rust-analyzer-cargo-watch-command "clippy" rustic-format-on-save t))
I also want to use clippy for linting, and those sweet org-mode docs
elisp
(use-package rustic :after lsp :config (setq lsp-rust-analyzer-cargo-watch-command "clippy" rustic-lsp-server 'rust-analyzer) (rustic-doc-mode t))
- Better Defaults
The defaults for emacs aren’t so good nowadays. Lets fix that up a bit
elisp
(setq undo-limit 80000000 ;I mess up too much evil-want-fine-undo t ;By default while in insert all changes are one big blob. Be more granular scroll-margin 2 ;having a little margin is nice auto-save-default t ;I dont like to lose work ;; display-line-numbers-type 'relative ;If I have to use line numbers, at least make them relative display-line-numbers-type nil ;I dislike line numbers history-length 25 ;Slight speedup delete-by-moving-to-trash t ;delete to system trash instead browser-url-browser-function 'eww-browse-url ; use the builtin eww to browse links truncate-string-ellipsis "…") ;default ellipses suck (fringe-mode 0) ;;disable fringe (global-subword-mode 1) ;;navigate through Camel Case words (tool-bar-mode +1) ;;re-enable the toolbar (global-so-long-mode -1) ;;i almost never want this on ;; emacs29 fixes (general-auto-unbind-keys :off) (remove-hook 'doom-after-init-modules-hook #'general-auto-unbind-keys)
There’s issues with emacs flickering on mac (and sometimes wayland). This should fix it
Instead of fundamental mode, lisp-interaction-mode seems much more useful
Ask where to open splits
…and open a buffer for it
elisp
(defadvice! prompt-for-buffer (&rest _) :after '(evil-window-split evil-window-vsplit) (consult-buffer))
The default bindings of doom are pretty good. I’m not so good with motions though, so lets make life easier with avy
elisp
(map! :leader :desc "hop to word" "w w" #'avy-goto-word-or-subword-1) (map! :leader :desc "hop to word" "w W" #'avy-goto-char-2) (map! :leader :desc "hop to line" "l" #'avy-goto-line)
I also fine ; more intuitive than : for entering command mode
When im doing regexes, its usually with /g anyways, lets make that the default
elisp
(after! evil (setq evil-ex-substitute-global t ; I like my s/../.. to by global by default evil-move-cursor-back nil ; Don't move the block cursor when toggling insert mode evil-kill-on-visual-paste nil)) ; Don't put overwritten text in the kill ring
Doom looks much cleaner with the dividers removed. Not sure why it isn’t the default honestly
elisp
(custom-set-faces! `(vertical-border :background ,(doom-color 'bg) :foreground ,(doom-color 'bg)))
I don’t like seeing the cursorline, especially while writing. Lets disable that
Doom has a weird bug with emacs-plus where the cursor will just turn white on a light theme. Lets fix that.
elisp
(defadvice! fix-+evil-default-cursor-fn () :override #'+evil-default-cursor-fn (evil-set-cursor-color (face-background 'cursor))) (defadvice! fix-+evil-emacs-cursor-fn () :override #'+evil-emacs-cursor-fn (evil-set-cursor-color (face-foreground 'warning)))
I like a bit of padding, both inside and outside, and lets make the line spacing comfier
elisp
(use-package frame :config (setq-default default-frame-alist (append (list '(internal-border-width . 24) '(left-fringe . 0) '(right-fringe . 0) '(tool-bar-lines . 0) '(menu-bar-lines . 0) '(line-spacing . 0.24) '(vertical-scroll-bars . nil)))) (setq-default window-resize-pixelwise t) (setq-default frame-resize-pixelwise t))
SelectricNK-Creams mode
Instead of using the regular selectric-mode, I modified it with a few notable tweaks, mainly:
- Support for EVIL mode
- It uses NK Cream sounds instead of the typewritter ones
The samples used here are taken from monketype, but heres a similar board
5.1.5. Visual configuration
- Treesitter
Nvim-treesitter is based on three interlocking features: language parsers, queries, and modules, where modules provide features – e.g., highlighting – based on queries for syntax objects extracted from a given buffer by language parsers. Allowing this to work in doom will reduce the lag introduced by fontlock as well as improve textobjects.
Since I use an apple silicon mac, I prefer if nix handles compiling the parsers for me
elisp
;; (use-package! tree-sitter ;; :config ;; (cl-pushnew (expand-file-name "~/.config/tree-sitter") tree-sitter-load-path) ;; (require 'tree-sitter-langs) ;; (global-tree-sitter-mode) ;; (add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode))
- Modeline
Doom modeline already looks good, but it can be better. Lets add some icons, the battery status, and make sure we don’t lose track of time
elisp
(after! doom-modeline (setq evil-normal-state-tag "<λ>" evil-insert-state-tag "<I>" evil-visual-state-tag "<V>" evil-motion-state-tag "<M>" evil-emacs-state-tag "<EMACS>") (setq doom-modeline-modal-icon nil doom-modeline-major-mode-icon t doom-modeline-major-mode-color-icon t doom-modeline-continuous-word-count-modes '(markdown-mode gfm-mode org-mode) doom-modeline-buffer-encoding nil inhibit-compacting-font-caches t find-file-visit-truename t) (custom-set-faces! '(doom-modeline-evil-insert-state :inherit doom-modeline-urgent) '(doom-modeline-evil-visual-state :inherit doom-modeline-warning) '(doom-modeline-evil-normal-state :inherit doom-modeline-buffer-path)) (setq doom-modeline-enable-word-count t)) ;Show word count
- Centaur tabs
There isn’t much of a point having tabs when you only have one buffer open. This checks the number of tabs, and hides them if theres only one left
elisp
(defun centaur-tabs-get-total-tab-length () (length (centaur-tabs-tabs (centaur-tabs-current-tabset)))) (defun centaur-tabs-hide-on-window-change () (run-at-time nil nil (lambda () (centaur-tabs-hide-check (centaur-tabs-get-total-tab-length))))) (defun centaur-tabs-hide-check (len) (shut-up (cond ((and (= len 1) (not (centaur-tabs-local-mode))) (call-interactively #'centaur-tabs-local-mode)) ((and (>= len 2) (centaur-tabs-local-mode)) (call-interactively #'centaur-tabs-local-mode)))))
I also like to have icons with my tabs.
elisp
(after! centaur-tabs (centaur-tabs-mode -1) (centaur-tabs-headline-match) (centaur-tabs-change-fonts "Liga SFMono Nerd Font" 150) (setq centaur-tabs-style "wave" centaur-tabs-set-icons t centaur-tabs-set-bar 'nil centaur-tabs-gray-out-icons 'buffer centaur-tabs-height 30 centaur-tabs-close-button "" centaur-tabs-modified-marker nil centaur-tabs-show-navigation-buttons nil centaur-tabs-show-new-tab-button nil centaur-tabs-down-tab-text " ✦" centaur-tabs-backward-tab-text " ⏴ " centaur-tabs-forward-tab-text " ⏵ ") (custom-set-faces! `(tab-line :background ,(doom-color 'base1) :foreground ,(doom-color 'base1)) `(centaur-tabs-default :background ,(doom-color 'base1) :foreground ,(doom-color 'base1)) `(centaur-tabs-active-bar-face :background ,(doom-color 'base1) :foreground ,(doom-color 'base1)) `(centaur-tabs-unselected-modified :background ,(doom-color 'base1) :foreground ,(doom-color 'fg)) `(centaur-tabs-unselected :background ,(doom-color 'base1) :foreground ,(doom-color 'base4)) `(centaur-tabs-selected-modified :background ,(doom-color 'bg) :foreground ,(doom-color 'fg)) `(centaur-tabs-selected :background ,(doom-color 'bg) :foreground ,(doom-color 'blue))) (add-hook 'window-configuration-change-hook 'centaur-tabs-hide-on-window-change))
- Vertico
For marginalia (vertico), lets use relative time, along with some other things
elisp
(after! marginalia (setq marginalia-censor-variables nil) (defadvice! +marginalia--anotate-local-file-colorful (cand) "Just a more colourful version of `marginalia--anotate-local-file'." :override #'marginalia--annotate-local-file (when-let (attrs (file-attributes (substitute-in-file-name (marginalia--full-candidate cand)) 'integer)) (marginalia--fields ((marginalia--file-owner attrs) :width 12 :face 'marginalia-file-owner) ((marginalia--file-modes attrs)) ((+marginalia-file-size-colorful (file-attribute-size attrs)) :width 7) ((+marginalia--time-colorful (file-attribute-modification-time attrs)) :width 12)))) (defun +marginalia--time-colorful (time) (let* ((seconds (float-time (time-subtract (current-time) time))) (color (doom-blend (face-attribute 'marginalia-date :foreground nil t) (face-attribute 'marginalia-documentation :foreground nil t) (/ 1.0 (log (+ 3 (/ (+ 1 seconds) 345600.0))))))) ;; 1 - log(3 + 1/(days + 1)) % grey (propertize (marginalia--time time) 'face (list :foreground color)))) (defun +marginalia-file-size-colorful (size) (let* ((size-index (/ (log10 (+ 1 size)) 7.0)) (color (if (< size-index 10000000) ; 10m (doom-blend 'orange 'green size-index) (doom-blend 'red 'orange (- size-index 1))))) (propertize (file-size-human-readable size) 'face (list :foreground color)))))
- Treemacs
Lets theme treemacs while we’re at it
- Emojis
Disable some annoying emojis
elisp
(defvar emojify-disabled-emojis '(;; Org "◼" "☑" "☸" "⚙" "⏩" "⏪" "⬆" "⬇" "❓" ;; Terminal powerline "✔" ;; Box drawing "▶" "◀") "Characters that should never be affected by `emojify-mode'.") (defadvice! emojify-delete-from-data () "Ensure `emojify-disabled-emojis' don't appear in `emojify-emojis'." :after #'emojify-set-emoji-data (dolist (emoji emojify-disabled-emojis) (remhash emoji emojify-emojis))) (add-hook! '(mu4e-compose-mode org-msg-edit-mode) (emoticon-to-emoji 1))
- Splash screen
Emacs can render an image as the splash screen, and the emacs logo looks pretty cool Now we just make it theme-appropriate, and resize with the frame.
Emacs Lisp
(defvar fancy-splash-image-template (expand-file-name "misc/splash-images/emacs-e-template.svg" doom-private-dir) "Default template svg used for the splash image, with substitutions from ") (defvar fancy-splash-sizes `((:height 300 :min-height 50 :padding (0 . 2)) (:height 250 :min-height 42 :padding (2 . 4)) (:height 200 :min-height 35 :padding (3 . 3)) (:height 150 :min-height 28 :padding (3 . 3)) (:height 100 :min-height 20 :padding (2 . 2)) (:height 75 :min-height 15 :padding (2 . 1)) (:height 50 :min-height 10 :padding (1 . 0)) (:height 1 :min-height 0 :padding (0 . 0))) "list of plists with the following properties :height the height of the image :min-height minimum `frame-height' for image :padding `+doom-dashboard-banner-padding' (top . bottom) to apply :template non-default template file :file file to use instead of template") (defvar fancy-splash-template-colours '(("$colour1" . keywords) ("$colour2" . type) ("$colour3" . base5) ("$colour4" . base8)) "list of colour-replacement alists of the form (\"$placeholder\" . 'theme-colour) which applied the template") (unless (file-exists-p (expand-file-name "theme-splashes" doom-cache-dir)) (make-directory (expand-file-name "theme-splashes" doom-cache-dir) t)) (defun fancy-splash-filename (theme-name height) (expand-file-name (concat (file-name-as-directory "theme-splashes") theme-name "-" (number-to-string height) ".svg") doom-cache-dir)) (defun fancy-splash-clear-cache () "Delete all cached fancy splash images" (interactive) (delete-directory (expand-file-name "theme-splashes" doom-cache-dir) t) (message "Cache cleared!")) (defun fancy-splash-generate-image (template height) "Read TEMPLATE and create an image if HEIGHT with colour substitutions as described by `fancy-splash-template-colours' for the current theme" (with-temp-buffer (insert-file-contents template) (re-search-forward "$height" nil t) (replace-match (number-to-string height) nil nil) (dolist (substitution fancy-splash-template-colours) (goto-char (point-min)) (while (re-search-forward (car substitution) nil t) (replace-match (doom-color (cdr substitution)) nil nil))) (write-region nil nil (fancy-splash-filename (symbol-name doom-theme) height) nil nil))) (defun fancy-splash-generate-images () "Perform `fancy-splash-generate-image' in bulk" (dolist (size fancy-splash-sizes) (unless (plist-get size :file) (fancy-splash-generate-image (or (plist-get size :template) fancy-splash-image-template) (plist-get size :height))))) (defun ensure-theme-splash-images-exist (&optional height) (unless (file-exists-p (fancy-splash-filename (symbol-name doom-theme) (or height (plist-get (car fancy-splash-sizes) :height)))) (fancy-splash-generate-images))) (defun get-appropriate-splash () (let ((height (frame-height))) (cl-some (lambda (size) (when (>= height (plist-get size :min-height)) size)) fancy-splash-sizes))) (setq fancy-splash-last-size nil) (setq fancy-splash-last-theme nil) (defun set-appropriate-splash (&rest _) (let ((appropriate-image (get-appropriate-splash))) (unless (and (equal appropriate-image fancy-splash-last-size) (equal doom-theme fancy-splash-last-theme))) (unless (plist-get appropriate-image :file) (ensure-theme-splash-images-exist (plist-get appropriate-image :height))) (setq fancy-splash-image (or (plist-get appropriate-image :file) (fancy-splash-filename (symbol-name doom-theme) (plist-get appropriate-image :height)))) (setq +doom-dashboard-banner-padding (plist-get appropriate-image :padding)) (setq fancy-splash-last-size appropriate-image) (setq fancy-splash-last-theme doom-theme) (+doom-dashboard-reload))) (add-hook 'window-size-change-functions #'set-appropriate-splash) (add-hook 'doom-load-theme-hook #'set-appropriate-splash)
Lets add a little phrase in there as well
elisp
(defvar splash-phrase-source-folder (expand-file-name "misc/splash-phrases" doom-private-dir) "A folder of text files with a fun phrase on each line.") (defvar splash-phrase-sources (let* ((files (directory-files splash-phrase-source-folder nil "\\.txt\\'")) (sets (delete-dups (mapcar (lambda (file) (replace-regexp-in-string "\\(?:-[0-9]+-\\w+\\)?\\.txt" "" file)) files)))) (mapcar (lambda (sset) (cons sset (delq nil (mapcar (lambda (file) (when (string-match-p (regexp-quote sset) file) file)) files)))) sets)) "A list of cons giving the phrase set name, and a list of files which contain phrase components.") (defvar splash-phrase-set (nth (random (length splash-phrase-sources)) (mapcar #'car splash-phrase-sources)) "The default phrase set. See `splash-phrase-sources'.") (defun splase-phrase-set-random-set () "Set a new random splash phrase set." (interactive) (setq splash-phrase-set (nth (random (1- (length splash-phrase-sources))) (cl-set-difference (mapcar #'car splash-phrase-sources) (list splash-phrase-set)))) (+doom-dashboard-reload t)) (defvar splase-phrase--cache nil) (defun splash-phrase-get-from-file (file) "Fetch a random line from FILE." (let ((lines (or (cdr (assoc file splase-phrase--cache)) (cdar (push (cons file (with-temp-buffer (insert-file-contents (expand-file-name file splash-phrase-source-folder)) (split-string (string-trim (buffer-string)) "\n"))) splase-phrase--cache))))) (nth (random (length lines)) lines))) (defun splash-phrase (&optional set) "Construct a splash phrase from SET. See `splash-phrase-sources'." (mapconcat #'splash-phrase-get-from-file (cdr (assoc (or set splash-phrase-set) splash-phrase-sources)) " ")) (defun doom-dashboard-phrase () "Get a splash phrase, flow it over multiple lines as needed, and make fontify it." (mapconcat (lambda (line) (+doom-dashboard--center +doom-dashboard--width (with-temp-buffer (insert-text-button line 'action (lambda (_) (+doom-dashboard-reload t)) 'face 'doom-dashboard-menu-title 'mouse-face 'doom-dashboard-menu-title 'help-echo "Random phrase" 'follow-link t) (buffer-string)))) (split-string (with-temp-buffer (insert (splash-phrase)) (setq fill-column (min 70 (/ (* 2 (window-width)) 3))) (fill-region (point-min) (point-max)) (buffer-string)) "\n") "\n")) (defadvice! doom-dashboard-widget-loaded-with-phrase () :override #'doom-dashboard-widget-loaded (setq line-spacing 0.2) (insert "\n\n" (propertize (+doom-dashboard--center +doom-dashboard--width (doom-display-benchmark-h 'return)) 'face 'doom-dashboard-loaded) "\n" (doom-dashboard-phrase) "\n"))
Lastly, the doom dashboard “useful commands” are no longer useful to me. So, we’ll disable them and then for a particularly clean look disable the modeline, then also hide the cursor.
Emacs Lisp
(remove-hook '+doom-dashboard-functions #'doom-dashboard-widget-shortmenu) (add-hook! '+doom-dashboard-mode-hook (hide-mode-line-mode 1) (hl-line-mode -1)) (setq-hook! '+doom-dashboard-mode-hook evil-normal-state-cursor (list nil))
- Writeroom
For starters, I think Doom is a bit over-zealous when zooming in
Then, when using Org it would be nice to make a number of other aesthetic tweaks. Namely:
- Use a serif-ed variable-pitch font
- Hiding headline leading stars
- Using fleurons as headline bullets
- Hiding line numbers
- Removing outline indentation
- Centering the text
- Disabling
doom-modeline
elisp
(defvar +zen-serif-p t "Whether to use a serifed font with `mixed-pitch-mode'.") (after! writeroom-mode (defvar-local +zen--original-org-indent-mode-p nil) (defvar-local +zen--original-mixed-pitch-mode-p nil) (defun +zen-enable-mixed-pitch-mode-h () "Enable `mixed-pitch-mode' when in `+zen-mixed-pitch-modes'." (when (apply #'derived-mode-p +zen-mixed-pitch-modes) (if writeroom-mode (progn (setq +zen--original-mixed-pitch-mode-p mixed-pitch-mode) (funcall (if +zen-serif-p #'mixed-pitch-serif-mode #'mixed-pitch-mode) 1)) (funcall #'mixed-pitch-mode (if +zen--original-mixed-pitch-mode-p 1 -1))))) (pushnew! writeroom--local-variables 'display-line-numbers 'visual-fill-column-width 'org-adapt-indentation 'org-superstar-headline-bullets-list 'org-superstar-remove-leading-stars) (add-hook 'writeroom-mode-enable-hook (defun +zen-prose-org-h () "Reformat the current Org buffer appearance for prose." (when (eq major-mode 'org-mode) (setq display-line-numbers nil visual-fill-column-width 60 org-adapt-indentation nil) (when (featurep 'org-superstar) (setq-local org-superstar-headline-bullets-list '("◉" "○" "✸" "✿" "✤" "✜" "◆" "▶") org-superstar-remove-leading-stars t) (org-superstar-restart)) (setq +zen--original-org-indent-mode-p org-indent-mode) (org-indent-mode -1)))) (add-hook! 'writeroom-mode-hook (if writeroom-mode (add-hook 'post-command-hook #'recenter nil t) (remove-hook 'post-command-hook #'recenter t))) (add-hook 'writeroom-mode-enable-hook #'doom-disable-line-numbers-h) (add-hook 'writeroom-mode-disable-hook #'doom-enable-line-numbers-h) (add-hook 'writeroom-mode-disable-hook (defun +zen-nonprose-org-h () "Reverse the effect of `+zen-prose-org'." (when (eq major-mode 'org-mode) (when (featurep 'org-superstar) (org-superstar-restart)) (when +zen--original-org-indent-mode-p (org-indent-mode 1))))))
- Font Display
Mixed pitch is great. As is
+org-pretty-mode
, let’s use them.However, the subscripts (and superscripts) are confusing with latex fragments, so lets turn those off
Let’s make headings a bit bigger
Emacs Lisp
(custom-set-faces! '(org-document-title :height 1.2) '(outline-1 :weight extra-bold :height 1.25) '(outline-2 :weight bold :height 1.15) '(outline-3 :weight bold :height 1.12) '(outline-4 :weight semi-bold :height 1.09) '(outline-5 :weight semi-bold :height 1.06) '(outline-6 :weight semi-bold :height 1.03) '(outline-8 :weight semi-bold) '(outline-9 :weight semi-bold))
It seems reasonable to have deadlines in the error face when they’re passed.
Emacs Lisp
(setq org-agenda-deadline-faces '((1.0 . error) (1.0 . org-warning) (0.5 . org-upcoming-deadline) (0.0 . org-upcoming-distant-deadline)))
We can then have quote blocks stand out a bit more by making them italic.
Emacs Lisp
(use-package! org-appear :hook (org-mode . org-appear-mode) :config (setq org-appear-autoemphasis t org-appear-autosubmarkers t org-appear-autolinks nil) (run-at-time nil nil #'org-appear--set-elements))
Org files can be rather nice to look at, particularly with some of the customizations here. This comes at a cost however, expensive font-lock. Feeling like you’re typing through molasses in large files is no fun, but there is a way I can defer font-locking when typing to make the experience more responsive.
Emacs Lisp
(defun locally-defer-font-lock () "Set jit-lock defer and stealth, when buffer is over a certain size." (when (> (buffer-size) 50000) (setq-local jit-lock-defer-time 0.05 jit-lock-stealth-time 1))) (add-hook 'org-mode-hook #'locally-defer-font-lock)
- Fontifying inline src blocks
Org does lovely things with #+begin_src blocks, like using font-lock for language’s major-mode behind the scenes and pulling out the lovely colourful results. By contrast, inline src_ blocks are somewhat neglected.
I am not the first person to feel this way, thankfully others have taken to stackexchange to voice their desire for inline src fontification. I was going to steal their work, but unfortunately they didn’t perform true source code fontification, but simply applied the org-code face to the content.
We can do better than that, and we shall! Using
org-src-font-lock-fontify-block
we can apply language-appropriate syntax highlighting. Then, continuing on to {{{results(...)}}} , it can have the org-block face applied to match, and then the value-surrounding constructs hidden by mimicking the behaviour ofprettify-symbols-mode
.Emacs Lisp
(defvar org-prettify-inline-results t "Whether to use (ab)use prettify-symbols-mode on {{{results(...)}}}. Either t or a cons cell of strings which are used as substitutions for the start and end of inline results, respectively.") (defvar org-fontify-inline-src-blocks-max-length 200 "Maximum content length of an inline src block that will be fontified.") (defun org-fontify-inline-src-blocks (limit) "Try to apply `org-fontify-inline-src-blocks-1'." (condition-case nil (org-fontify-inline-src-blocks-1 limit) (error (message "Org mode fontification error in %S at %d" (current-buffer) (line-number-at-pos))))) (defun org-fontify-inline-src-blocks-1 (limit) "Fontify inline src_LANG blocks, from `point' up to LIMIT." (let ((case-fold-search t) (initial-point (point))) (while (re-search-forward "\\_<src_\\([^ \t\n[{]+\\)[{[]?" limit t) ; stolen from `org-element-inline-src-block-parser' (let ((beg (match-beginning 0)) pt (lang-beg (match-beginning 1)) (lang-end (match-end 1))) (remove-text-properties beg lang-end '(face nil)) (font-lock-append-text-property lang-beg lang-end 'face 'org-meta-line) (font-lock-append-text-property beg lang-beg 'face 'shadow) (font-lock-append-text-property beg lang-end 'face 'org-block) (setq pt (goto-char lang-end)) ;; `org-element--parse-paired-brackets' doesn't take a limit, so to ;; prevent it searching the entire rest of the buffer we temporarily ;; narrow the active region. (save-restriction (narrow-to-region beg (min (point-max) limit (+ lang-end org-fontify-inline-src-blocks-max-length))) (when (ignore-errors (org-element--parse-paired-brackets ?\[)) (remove-text-properties pt (point) '(face nil)) (font-lock-append-text-property pt (point) 'face 'org-block) (setq pt (point))) (when (ignore-errors (org-element--parse-paired-brackets ?\{)) (remove-text-properties pt (point) '(face nil)) (font-lock-append-text-property pt (1+ pt) 'face '(org-block shadow)) (unless (= (1+ pt) (1- (point))) (if org-src-fontify-natively (org-src-font-lock-fontify-block (buffer-substring-no-properties lang-beg lang-end) (1+ pt) (1- (point))) (font-lock-append-text-property (1+ pt) (1- (point)) 'face 'org-block))) (font-lock-append-text-property (1- (point)) (point) 'face '(org-block shadow)) (setq pt (point)))) (when (and org-prettify-inline-results (re-search-forward "\\= {{{results(" limit t)) (font-lock-append-text-property pt (1+ pt) 'face 'org-block) (goto-char pt)))) (when org-prettify-inline-results (goto-char initial-point) (org-fontify-inline-src-results limit)))) (defun org-fontify-inline-src-results (limit) (while (re-search-forward "{{{results(\\(.+?\\))}}}" limit t) (remove-list-of-text-properties (match-beginning 0) (point) '(composition prettify-symbols-start prettify-symbols-end)) (font-lock-append-text-property (match-beginning 0) (match-end 0) 'face 'org-block) (let ((start (match-beginning 0)) (end (match-beginning 1))) (with-silent-modifications (compose-region start end (if (eq org-prettify-inline-results t) "⟨" (car org-prettify-inline-results))) (add-text-properties start end `(prettify-symbols-start ,start prettify-symbols-end ,end)))) (let ((start (match-end 1)) (end (point))) (with-silent-modifications (compose-region start end (if (eq org-prettify-inline-results t) "⟩" (cdr org-prettify-inline-results))) (add-text-properties start end `(prettify-symbols-start ,start prettify-symbols-end ,end)))))) (defun org-fontify-inline-src-blocks-enable () "Add inline src fontification to font-lock in Org. Must be run as part of `org-font-lock-set-keywords-hook'." (setq org-font-lock-extra-keywords (append org-font-lock-extra-keywords '((org-fontify-inline-src-blocks))))) (add-hook 'org-font-lock-set-keywords-hook #'org-fontify-inline-src-blocks-enable)
- Fontifying inline src blocks
- Symbols
Firstly, I dislike the default stars for org-mode, so lets improve that
Emacs Lisp
;;make bullets look better (after! org-superstar (setq org-superstar-headline-bullets-list '("◉" "○" "✸" "✿" "✤" "✜" "◆" "▶") org-superstar-prettify-item-bullets t ))
I also want to hide leading stars, since they feel redundant
elisp
(setq org-ellipsis " ▾ " org-hide-leading-stars t org-priority-highest ?A org-priority-lowest ?E org-priority-faces '((?A . 'all-the-icons-red) (?B . 'all-the-icons-orange) (?C . 'all-the-icons-yellow) (?D . 'all-the-icons-green) (?E . 'all-the-icons-blue)))
Lastly, lets add some ligatures for some org mode stuff
elisp
(appendq! +ligatures-extra-symbols `(:checkbox "☐" :pending "◼" :checkedbox "☑" :list_property "∷" :em_dash "—" :ellipses "…" :arrow_right "→" :arrow_left "←" :property "☸" :options "⌥" :startup "⏻" :html_head "🅷" :html "🅗" :latex_class "🄻" :latex_header "🅻" :beamer_header "🅑" :latex "🅛" :attr_latex "🄛" :attr_html "🄗" :attr_org "⒪" :begin_quote "❝" :end_quote "❞" :caption "☰" :header "›" :begin_export "⏩" :end_export "⏪" :properties "⚙" :end "∎" :priority_a ,(propertize "⚑" 'face 'all-the-icons-red) :priority_b ,(propertize "⬆" 'face 'all-the-icons-orange) :priority_c ,(propertize "■" 'face 'all-the-icons-yellow) :priority_d ,(propertize "⬇" 'face 'all-the-icons-green) :priority_e ,(propertize "❓" 'face 'all-the-icons-blue))) (set-ligatures! 'org-mode :merge t :checkbox "[ ]" :pending "[-]" :checkedbox "[X]" :list_property "::" :em_dash "---" :ellipsis "..." :arrow_right "->" :arrow_left "<-" :title "#+title:" :subtitle "#+subtitle:" :author "#+author:" :date "#+date:" :property "#+property:" :options "#+options:" :startup "#+startup:" :macro "#+macro:" :html_head "#+html_head:" :html "#+html:" :latex_class "#+latex_class:" :latex_header "#+latex_header:" :beamer_header "#+beamer_header:" :latex "#+latex:" :attr_latex "#+attr_latex:" :attr_html "#+attr_html:" :attr_org "#+attr_org:" :begin_quote "#+begin_quote" :end_quote "#+end_quote" :caption "#+caption:" :header "#+header:" :begin_export "#+begin_export" :end_export "#+end_export" :results "#+RESULTS:" :property ":PROPERTIES:" :end ":END:" :priority_a "[#A]" :priority_b "[#B]" :priority_c "[#C]" :priority_d "[#D]" :priority_e "[#E]") (plist-put +ligatures-extra-symbols :name "⁍")
Lets also add a function that makes it easy to convert from upper to lowercase, since the ligatures don’t work with Uppercase (I can make them work, but lowercase looks better anyways)
elisp
(defun org-syntax-convert-keyword-case-to-lower () "Convert all #+KEYWORDS to #+keywords." (interactive) (save-excursion (goto-char (point-min)) (let ((count 0) (case-fold-search nil)) (while (re-search-forward "^[ \t]*#\\+[A-Z_]+" nil t) (unless (s-matches-p "RESULTS" (match-string 0)) (replace-match (downcase (match-string 0)) t) (setq count (1+ count)))) (message "Replaced %d occurances" count))))
- Keycast
Its nice for demonstrations
elisp
(use-package! keycast :commands keycast-mode :config (define-minor-mode keycast-mode "Show current command and its key binding in the mode line." :global t (if keycast-mode (progn (add-hook 'pre-command-hook 'keycast--update t) (add-to-list 'global-mode-string '("" mode-line-keycast " "))) (remove-hook 'pre-command-hook 'keycast--update) (setq global-mode-string (remove '("" mode-line-keycast " ") global-mode-string)))) (custom-set-faces! '(keycast-command :inherit doom-modeline-debug :height 1.0) '(keycast-key :inherit custom-modified :height 1.0 :weight bold)))
- Transparency
I’m not too big of a fan of transparency, but some people like it. You can use this little function to toggle it now. On C-c t inactive windows will dim (85% transparency) and focused windows remain opaque
elisp
(defun toggle-transparency () (interactive) (let ((alpha (frame-parameter nil 'alpha))) (set-frame-parameter nil 'alpha (if (eql (cond ((numberp alpha) alpha) ((numberp (cdr alpha)) (cdr alpha)) ;; Also handle undocumented (<active> <inactive>) form. ((numberp (cadr alpha)) (cadr alpha))) 100) '(100 . 85) '(100 . 100))))) (global-set-key (kbd "C-c t") 'toggle-transparency)
- RSS
RSS is a nice simple way of getting my news. Lets set that up
elisp
(map! :map elfeed-search-mode-map :after elfeed-search [remap kill-this-buffer] "q" [remap kill-buffer] "q" :n doom-leader-key nil :n "q" #'+rss/quit :n "e" #'elfeed-update :n "r" #'elfeed-search-untag-all-unread :n "u" #'elfeed-search-tag-all-unread :n "s" #'elfeed-search-live-filter :n "RET" #'elfeed-search-show-entry :n "p" #'elfeed-show-pdf :n "+" #'elfeed-search-tag-all :n "-" #'elfeed-search-untag-all :n "S" #'elfeed-search-set-filter :n "b" #'elfeed-search-browse-url :n "y" #'elfeed-search-yank) (map! :map elfeed-show-mode-map :after elfeed-show [remap kill-this-buffer] "q" [remap kill-buffer] "q" :n doom-leader-key nil :nm "q" #'+rss/delete-pane :nm "o" #'ace-link-elfeed :nm "RET" #'org-ref-elfeed-add :nm "n" #'elfeed-show-next :nm "N" #'elfeed-show-prev :nm "p" #'elfeed-show-pdf :nm "+" #'elfeed-show-tag :nm "-" #'elfeed-show-untag :nm "s" #'elfeed-show-new-live-search :nm "y" #'elfeed-show-yank) (after! elfeed-search (set-evil-initial-state! 'elfeed-search-mode 'normal)) (after! elfeed-show-mode (set-evil-initial-state! 'elfeed-show-mode 'normal)) (after! evil-snipe (push 'elfeed-show-mode evil-snipe-disabled-modes) (push 'elfeed-search-mode evil-snipe-disabled-modes)) (after! elfeed (elfeed-org) (use-package! elfeed-link) (setq elfeed-search-filter "@1-week-ago +unread" elfeed-search-print-entry-function '+rss/elfeed-search-print-entry elfeed-search-title-min-width 80 elfeed-show-entry-switch #'pop-to-buffer elfeed-show-entry-delete #'+rss/delete-pane elfeed-show-refresh-function #'+rss/elfeed-show-refresh--better-style shr-max-image-proportion 0.6) (add-hook! 'elfeed-show-mode-hook (hide-mode-line-mode 1)) (add-hook! 'elfeed-search-update-hook #'hide-mode-line-mode) (defface elfeed-show-title-face '((t (:weight ultrabold :slant italic :height 1.5))) "title face in elfeed show buffer" :group 'elfeed) (defface elfeed-show-author-face `((t (:weight light))) "title face in elfeed show buffer" :group 'elfeed) (set-face-attribute 'elfeed-search-title-face nil :foreground 'nil :weight 'light) (defadvice! +rss-elfeed-wrap-h-nicer () "Enhances an elfeed entry's readability by wrapping it to a width of `fill-column' and centering it with `visual-fill-column-mode'." :override #'+rss-elfeed-wrap-h (setq-local truncate-lines nil shr-width 120 visual-fill-column-center-text t default-text-properties '(line-height 1.1)) (let ((inhibit-read-only t) (inhibit-modification-hooks t)) (visual-fill-column-mode) ;; (setq-local shr-current-font '(:family "Merriweather" :height 1.2)) (set-buffer-modified-p nil))) (defun +rss/elfeed-search-print-entry (entry) "Print ENTRY to the buffer." (let* ((elfeed-goodies/tag-column-width 40) (elfeed-goodies/feed-source-column-width 30) (title (or (elfeed-meta entry :title) (elfeed-entry-title entry) "")) (title-faces (elfeed-search--faces (elfeed-entry-tags entry))) (feed (elfeed-entry-feed entry)) (feed-title (when feed (or (elfeed-meta feed :title) (elfeed-feed-title feed)))) (tags (mapcar #'symbol-name (elfeed-entry-tags entry))) (tags-str (concat (mapconcat 'identity tags ","))) (title-width (- (window-width) elfeed-goodies/feed-source-column-width elfeed-goodies/tag-column-width 4)) (tag-column (elfeed-format-column tags-str (elfeed-clamp (length tags-str) elfeed-goodies/tag-column-width elfeed-goodies/tag-column-width) :left)) (feed-column (elfeed-format-column feed-title (elfeed-clamp elfeed-goodies/feed-source-column-width elfeed-goodies/feed-source-column-width elfeed-goodies/feed-source-column-width) :left))) (insert (propertize feed-column 'face 'elfeed-search-feed-face) " ") (insert (propertize tag-column 'face 'elfeed-search-tag-face) " ") (insert (propertize title 'face title-faces 'kbd-help title)) (setq-local line-spacing 0.2))) (defun +rss/elfeed-show-refresh--better-style () "Update the buffer to match the selected entry, using a mail-style." (interactive) (let* ((inhibit-read-only t) (title (elfeed-entry-title elfeed-show-entry)) (date (seconds-to-time (elfeed-entry-date elfeed-show-entry))) (author (elfeed-meta elfeed-show-entry :author)) (link (elfeed-entry-link elfeed-show-entry)) (tags (elfeed-entry-tags elfeed-show-entry)) (tagsstr (mapconcat #'symbol-name tags ", ")) (nicedate (format-time-string "%a, %e %b %Y %T %Z" date)) (content (elfeed-deref (elfeed-entry-content elfeed-show-entry))) (type (elfeed-entry-content-type elfeed-show-entry)) (feed (elfeed-entry-feed elfeed-show-entry)) (feed-title (elfeed-feed-title feed)) (base (and feed (elfeed-compute-base (elfeed-feed-url feed))))) (erase-buffer) (insert "\n") (insert (format "%s\n\n" (propertize title 'face 'elfeed-show-title-face))) (insert (format "%s\t" (propertize feed-title 'face 'elfeed-search-feed-face))) (when (and author elfeed-show-entry-author) (insert (format "%s\n" (propertize author 'face 'elfeed-show-author-face)))) (insert (format "%s\n\n" (propertize nicedate 'face 'elfeed-log-date-face))) (when tags (insert (format "%s\n" (propertize tagsstr 'face 'elfeed-search-tag-face)))) ;; (insert (propertize "Link: " 'face 'message-header-name)) ;; (elfeed-insert-link link link) ;; (insert "\n") (cl-loop for enclosure in (elfeed-entry-enclosures elfeed-show-entry) do (insert (propertize "Enclosure: " 'face 'message-header-name)) do (elfeed-insert-link (car enclosure)) do (insert "\n")) (insert "\n") (if content (if (eq type 'html) (elfeed-insert-html content base) (insert content)) (insert (propertize "(empty)\n" 'face 'italic))) (goto-char (point-min))))) (after! elfeed-show (require 'url) (defvar elfeed-pdf-dir (expand-file-name "pdfs/" (file-name-directory (directory-file-name elfeed-enclosure-default-dir)))) (defvar elfeed-link-pdfs '(("https://www.jstatsoft.org/index.php/jss/article/view/v0\\([^/]+\\)" . "https://www.jstatsoft.org/index.php/jss/article/view/v0\\1/v\\1.pdf") ("http://arxiv.org/abs/\\([^/]+\\)" . "https://arxiv.org/pdf/\\1.pdf")) "List of alists of the form (REGEX-FOR-LINK . FORM-FOR-PDF)") (defun elfeed-show-pdf (entry) (interactive (list (or elfeed-show-entry (elfeed-search-selected :ignore-region)))) (let ((link (elfeed-entry-link entry)) (feed-name (plist-get (elfeed-feed-meta (elfeed-entry-feed entry)) :title)) (title (elfeed-entry-title entry)) (file-view-function (lambda (f) (when elfeed-show-entry (elfeed-kill-buffer)) (pop-to-buffer (find-file-noselect f)))) pdf) (let ((file (expand-file-name (concat (subst-char-in-string ?/ ?, title) ".pdf") (expand-file-name (subst-char-in-string ?/ ?, feed-name) elfeed-pdf-dir)))) (if (file-exists-p file) (funcall file-view-function file) (dolist (link-pdf elfeed-link-pdfs) (when (and (string-match-p (car link-pdf) link) (not pdf)) (setq pdf (replace-regexp-in-string (car link-pdf) (cdr link-pdf) link)))) (if (not pdf) (message "No associated PDF for entry") (message "Fetching %s" pdf) (unless (file-exists-p (file-name-directory file)) (make-directory (file-name-directory file) t)) (url-copy-file pdf file) (funcall file-view-function file)))))))
- Ebooks
To actually read the ebooks we use nov.
Emacs Lisp
(use-package! nov :mode ("\\.epub\\'" . nov-mode) :config (map! :map nov-mode-map :n "RET" #'nov-scroll-up) (advice-add 'nov-render-title :override #'ignore) (defun +nov-mode-setup () (face-remap-add-relative 'variable-pitch :family "Overpass" :height 1.4 :width 'semi-expanded) (face-remap-add-relative 'default :height 1.3) (setq-local line-spacing 0.2 next-screen-context-lines 4 shr-use-colors nil) (require 'visual-fill-column nil t) (setq-local visual-fill-column-center-text t visual-fill-column-width 81 nov-text-width 80) (visual-fill-column-mode 1) (add-to-list '+lookup-definition-functions #'+lookup/dictionary-definition) (add-hook 'nov-mode-hook #'+nov-mode-setup)))
- Screenshot
Testing
5.1.6. Org
- Org-Mode
Org mode is the best writing format, no contest. The defaults are more terminal-oriented, so lets make it look a little better
Some hooks are a bit annoying, so lets make them shut up
elisp
(defadvice! shut-up-org-problematic-hooks (orig-fn &rest args) :around #'org-fancy-priorities-mode :around #'org-superstar-mode (ignore-errors (apply orig-fn args)))
Sadly I can’t always work in org, but I can import stuff into it!
I prefer /org as my directory. Lets change some other defaults too
elisp
(setq org-directory "~/org" ; let's put files here org-use-property-inheritance t ; it's convenient to have properties inherited org-log-done 'time ; having the time a item is done sounds convenient org-list-allow-alphabetical t ; have a. A. a) A) list bullets org-export-in-background t ; run export processes in external emacs process org-catch-invisible-edits 'smart) ; try not to accidently do weird stuff in invisible regions
I want to slightly change the default args for babel
elisp
(setq org-babel-default-header-args '((:session . "none") (:results . "replace") (:exports . "code") (:cache . "no") (:noweb . "no") (:hlines . "no") (:tangle . "no") (:comments . "link")))
I also want to change the order of bullets
The [[yt:...]] links preview nicely, but don’t export nicely. Thankfully, we can fix that.
elisp
(after! ox (org-link-set-parameters "yt" :export #'+org-export-yt) (defun +org-export-yt (path desc backend _com) (cond ((org-export-derived-backend-p backend 'html) (format "<iframe width='440' \ height='335' \ src='https://www.youtube.com/embed/%s' \ frameborder='0' \ allowfullscreen>%s</iframe>" path (or "" desc))) ((org-export-derived-backend-p backend 'latex) (format "\\href{https://youtu.be/%s}{%s}" path (or desc "youtube"))) (t (format "https://youtu.be/%s" path)))))
- HTML
:header-args:emacs-lisp: :noweb-ref ox-html-conf
For some reason this only works if you have org first
elisp
(after! ox-html (define-minor-mode org-fancy-html-export-mode "Toggle my fabulous org export tweaks. While this mode itself does a little bit, the vast majority of the change in behaviour comes from switch statements in: - `org-html-template-fancier' - `org-html--build-meta-info-extended' - `org-html-src-block-collapsable' - `org-html-block-collapsable' - `org-html-table-wrapped' - `org-html--format-toc-headline-colapseable' - `org-html--toc-text-stripped-leaves' - `org-export-html-headline-anchor'" :global t :init-value t (if org-fancy-html-export-mode (setq org-html-style-default org-html-style-fancy org-html-meta-tags #'org-html-meta-tags-fancy org-html-checkbox-type 'html-span) (setq org-html-style-default org-html-style-plain org-html-meta-tags #'org-html-meta-tags-default org-html-checkbox-type 'html))) (defadvice! org-html-template-fancier (orig-fn contents info) "Return complete document string after HTML conversion. CONTENTS is the transcoded contents string. INFO is a plist holding export options. Adds a few extra things to the body compared to the default implementation." :around #'org-html-template (if (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress)) (funcall orig-fn contents info) (concat (when (and (not (org-html-html5-p info)) (org-html-xhtml-p info)) (let* ((xml-declaration (plist-get info :html-xml-declaration)) (decl (or (and (stringp xml-declaration) xml-declaration) (cdr (assoc (plist-get info :html-extension) xml-declaration)) (cdr (assoc "html" xml-declaration)) ""))) (when (not (or (not decl) (string= "" decl))) (format "%s\n" (format decl (or (and org-html-coding-system (fboundp 'coding-system-get) (coding-system-get org-html-coding-system 'mime-charset)) "iso-8859-1")))))) (org-html-doctype info) "\n" (concat "<html" (cond ((org-html-xhtml-p info) (format " xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\"" (plist-get info :language) (plist-get info :language))) ((org-html-html5-p info) (format " lang=\"%s\"" (plist-get info :language)))) ">\n") "<head>\n" (org-html--build-meta-info info) (org-html--build-head info) (org-html--build-mathjax-config info) "</head>\n" "<body>\n<input type='checkbox' id='theme-switch'><div id='page'><label id='switch-label' for='theme-switch'></label>" (let ((link-up (org-trim (plist-get info :html-link-up))) (link-home (org-trim (plist-get info :html-link-home)))) (unless (and (string= link-up "") (string= link-home "")) (format (plist-get info :html-home/up-format) (or link-up link-home) (or link-home link-up)))) ;; Preamble. (org-html--build-pre/postamble 'preamble info) ;; Document contents. (let ((div (assq 'content (plist-get info :html-divs)))) (format "<%s id=\"%s\">\n" (nth 1 div) (nth 2 div))) ;; Document title. (when (plist-get info :with-title) (let ((title (and (plist-get info :with-title) (plist-get info :title))) (subtitle (plist-get info :subtitle)) (html5-fancy (org-html--html5-fancy-p info))) (when title (format (if html5-fancy "<header class=\"page-header\">%s\n<h1 class=\"title\">%s</h1>\n%s</header>" "<h1 class=\"title\">%s%s</h1>\n") (if (or (plist-get info :with-date) (plist-get info :with-author)) (concat "<div class=\"page-meta\">" (when (plist-get info :with-date) (org-export-data (plist-get info :date) info)) (when (and (plist-get info :with-date) (plist-get info :with-author)) ", ") (when (plist-get info :with-author) (org-export-data (plist-get info :author) info)) "</div>\n") "") (org-export-data title info) (if subtitle (format (if html5-fancy "<p class=\"subtitle\" role=\"doc-subtitle\">%s</p>\n" (concat "\n" (org-html-close-tag "br" nil info) "\n" "<span class=\"subtitle\">%s</span>\n")) (org-export-data subtitle info)) ""))))) contents (format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs)))) ;; Postamble. (org-html--build-pre/postamble 'postamble info) ;; Possibly use the Klipse library live code blocks. (when (plist-get info :html-klipsify-src) (concat "<script>" (plist-get info :html-klipse-selection-script) "</script><script src=\"" org-html-klipse-js "\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\"" org-html-klipse-css "\"/>")) ;; Closing document. "</div>\n</body>\n</html>"))) (defadvice! org-html-toc-linked (depth info &optional scope) "Build a table of contents. Just like `org-html-toc', except the header is a link to \"#\". DEPTH is an integer specifying the depth of the table. INFO is a plist used as a communication channel. Optional argument SCOPE is an element defining the scope of the table. Return the table of contents as a string, or nil if it is empty." :override #'org-html-toc (let ((toc-entries (mapcar (lambda (headline) (cons (org-html--format-toc-headline headline info) (org-export-get-relative-level headline info))) (org-export-collect-headlines info depth scope)))) (when toc-entries (let ((toc (concat "<div id=\"text-table-of-contents\">" (org-html--toc-text toc-entries) "</div>\n"))) (if scope toc (let ((outer-tag (if (org-html--html5-fancy-p info) "nav" "div"))) (concat (format "<%s id=\"table-of-contents\">\n" outer-tag) (let ((top-level (plist-get info :html-toplevel-hlevel))) (format "<h%d><a href=\"#\" style=\"color:inherit; text-decoration: none;\">%s</a></h%d>\n" top-level (org-html--translate "Table of Contents" info) top-level)) toc (format "</%s>\n" outer-tag)))))))) (defvar org-html-meta-tags-opengraph-image '(:image "https://tecosaur.com/resources/org/nib.png" :type "image/png" :width "200" :height "200" :alt "Green fountain pen nib") "Plist of og:image:PROP properties and their value, for use in `org-html-meta-tags-fancy'.") (defun org-html-meta-tags-fancy (info) "Use the INFO plist to construct the meta tags, as described in `org-html-meta-tags'." (let ((title (org-html-plain-text (org-element-interpret-data (plist-get info :title)) info)) (author (and (plist-get info :with-author) (let ((auth (plist-get info :author))) ;; Return raw Org syntax. (and auth (org-html-plain-text (org-element-interpret-data auth) info)))))) (append (list (when (org-string-nw-p author) (list "name" "author" author)) (when (org-string-nw-p (plist-get info :description)) (list "name" "description" (plist-get info :description))) '("name" "generator" "org mode") '("name" "theme-color" "#77aa99") '("property" "og:type" "article") (list "property" "og:title" title) (let ((subtitle (org-export-data (plist-get info :subtitle) info))) (when (org-string-nw-p subtitle) (list "property" "og:description" subtitle)))) (when org-html-meta-tags-opengraph-image (list (list "property" "og:image" (plist-get org-html-meta-tags-opengraph-image :image)) (list "property" "og:image:type" (plist-get org-html-meta-tags-opengraph-image :type)) (list "property" "og:image:width" (plist-get org-html-meta-tags-opengraph-image :width)) (list "property" "og:image:height" (plist-get org-html-meta-tags-opengraph-image :height)) (list "property" "og:image:alt" (plist-get org-html-meta-tags-opengraph-image :alt)))) (list (when (org-string-nw-p author) (list "property" "og:article:author:first_name" (car (s-split-up-to " " author 2)))) (when (and (org-string-nw-p author) (s-contains-p " " author)) (list "property" "og:article:author:last_name" (cadr (s-split-up-to " " author 2)))) (list "property" "og:article:published_time" (format-time-string "%FT%T%z" (or (when-let ((date-str (cadar (org-collect-keywords '("DATE"))))) (unless (string= date-str (format-time-string "%F")) (ignore-errors (encode-time (org-parse-time-string date-str))))) (if buffer-file-name (file-attribute-modification-time (file-attributes buffer-file-name)) (current-time))))) (when buffer-file-name (list "property" "og:article:modified_time" (format-time-string "%FT%T%z" (file-attribute-modification-time (file-attributes buffer-file-name))))))))) (unless (functionp #'org-html-meta-tags-default) (defalias 'org-html-meta-tags-default #'ignore)) (setq org-html-meta-tags #'org-html-meta-tags-fancy) (setq org-html-style-plain org-html-style-default org-html-htmlize-output-type 'css org-html-doctype "html5" org-html-html5-fancy t) (defun org-html-reload-fancy-style () (interactive) (setq org-html-style-fancy (concat (f-read-text (expand-file-name "misc/org-export-header.html" doom-private-dir)) "<script>\n" (f-read-text (expand-file-name "misc/org-css/main.js" doom-private-dir)) "</script>\n<style>\n" (f-read-text (expand-file-name "misc/org-css/main.min.css" doom-private-dir)) "</style>")) (when org-fancy-html-export-mode (setq org-html-style-default org-html-style-fancy))) (org-html-reload-fancy-style) (defvar org-html-export-collapsed nil) (eval '(cl-pushnew '(:collapsed "COLLAPSED" "collapsed" org-html-export-collapsed t) (org-export-backend-options (org-export-get-backend 'html)))) (add-to-list 'org-default-properties "EXPORT_COLLAPSED") (defadvice! org-html-src-block-collapsable (orig-fn src-block contents info) "Wrap the usual <pre> block in a <details>" :around #'org-html-src-block (if (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress)) (funcall orig-fn src-block contents info) (let* ((properties (cadr src-block)) (lang (mode-name-to-lang-name (plist-get properties :language))) (name (plist-get properties :name)) (ref (org-export-get-reference src-block info)) (collapsed-p (member (or (org-export-read-attribute :attr_html src-block :collapsed) (plist-get info :collapsed)) '("y" "yes" "t" t "true" "all")))) (format "<details id='%s' class='code'%s><summary%s>%s</summary> <div class='gutter'> <a href='#%s'>#</a> <button title='Copy to clipboard' onclick='copyPreToClipbord(this)'>⎘</button>\ </div> %s </details>" ref (if collapsed-p "" " open") (if name " class='named'" "") (concat (when name (concat "<span class=\"name\">" name "</span>")) "<span class=\"lang\">" lang "</span>") ref (if name (replace-regexp-in-string (format "<pre\\( class=\"[^\"]+\"\\)? id=\"%s\">" ref) "<pre\\1>" (funcall orig-fn src-block contents info)) (funcall orig-fn src-block contents info)))))) (defun mode-name-to-lang-name (mode) (or (cadr (assoc mode '(("asymptote" "Asymptote") ("awk" "Awk") ("C" "C") ("clojure" "Clojure") ("css" "CSS") ("D" "D") ("ditaa" "ditaa") ("dot" "Graphviz") ("calc" "Emacs Calc") ("emacs-lisp" "Emacs Lisp") ("fortran" "Fortran") ("gnuplot" "gnuplot") ("haskell" "Haskell") ("hledger" "hledger") ("java" "Java") ("js" "Javascript") ("latex" "LaTeX") ("ledger" "Ledger") ("lisp" "Lisp") ("lilypond" "Lilypond") ("lua" "Lua") ("matlab" "MATLAB") ("mscgen" "Mscgen") ("ocaml" "Objective Caml") ("octave" "Octave") ("org" "Org mode") ("oz" "OZ") ("plantuml" "Plantuml") ("processing" "Processing.js") ("python" "Python") ("R" "R") ("ruby" "Ruby") ("sass" "Sass") ("scheme" "Scheme") ("screen" "Gnu Screen") ("sed" "Sed") ("sh" "shell") ("sql" "SQL") ("sqlite" "SQLite") ("forth" "Forth") ("io" "IO") ("J" "J") ("makefile" "Makefile") ("maxima" "Maxima") ("perl" "Perl") ("picolisp" "Pico Lisp") ("scala" "Scala") ("shell" "Shell Script") ("ebnf2ps" "ebfn2ps") ("cpp" "C++") ("abc" "ABC") ("coq" "Coq") ("groovy" "Groovy") ("bash" "bash") ("csh" "csh") ("ash" "ash") ("dash" "dash") ("ksh" "ksh") ("mksh" "mksh") ("posh" "posh") ("ada" "Ada") ("asm" "Assembler") ("caml" "Caml") ("delphi" "Delphi") ("html" "HTML") ("idl" "IDL") ("mercury" "Mercury") ("metapost" "MetaPost") ("modula-2" "Modula-2") ("pascal" "Pascal") ("ps" "PostScript") ("prolog" "Prolog") ("simula" "Simula") ("tcl" "tcl") ("tex" "LaTeX") ("plain-tex" "TeX") ("verilog" "Verilog") ("vhdl" "VHDL") ("xml" "XML") ("nxml" "XML") ("conf" "Configuration File")))) mode)) (defadvice! org-html-table-wrapped (orig-fn table contents info) "Wrap the usual <table> in a <div>" :around #'org-html-table (if (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress)) (funcall orig-fn table contents info) (let* ((name (plist-get (cadr table) :name)) (ref (org-export-get-reference table info))) (format "<div id='%s' class='table'> <div class='gutter'><a href='#%s'>#</a></div> <div class='tabular'> %s </div>\ </div>" ref ref (if name (replace-regexp-in-string (format "<table id=\"%s\"" ref) "<table" (funcall orig-fn table contents info)) (funcall orig-fn table contents info)))))) (defadvice! org-html--format-toc-headline-colapseable (orig-fn headline info) "Add a label and checkbox to `org-html--format-toc-headline's usual output, to allow the TOC to be a collapseable tree." :around #'org-html--format-toc-headline (if (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress)) (funcall orig-fn headline info) (let ((id (or (org-element-property :CUSTOM_ID headline) (org-export-get-reference headline info)))) (format "<input type='checkbox' id='toc--%s'/><label for='toc--%s'>%s</label>" id id (funcall orig-fn headline info))))) (defadvice! org-html--toc-text-stripped-leaves (orig-fn toc-entries) "Remove label" :around #'org-html--toc-text (if (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress)) (funcall orig-fn toc-entries) (replace-regexp-in-string "<input [^>]+><label [^>]+>\\(.+?\\)</label></li>" "\\1</li>" (funcall orig-fn toc-entries)))) (setq org-html-text-markup-alist '((bold . "<b>%s</b>") (code . "<code>%s</code>") (italic . "<i>%s</i>") (strike-through . "<del>%s</del>") (underline . "<span class=\"underline\">%s</span>") (verbatim . "<kbd>%s</kbd>"))) (appendq! org-html-checkbox-types '((html-span (on . "<span class='checkbox'></span>") (off . "<span class='checkbox'></span>") (trans . "<span class='checkbox'></span>")))) (setq org-html-checkbox-type 'html-span) (pushnew! org-html-special-string-regexps '("->" . "→") '("<-" . "←")) (defun org-export-html-headline-anchor (text backend info) (when (and (org-export-derived-backend-p backend 'html) (not (org-export-derived-backend-p backend 're-reveal)) org-fancy-html-export-mode) (unless (bound-and-true-p org-msg-export-in-progress) (replace-regexp-in-string "<h\\([0-9]\\) id=\"\\([a-z0-9-]+\\)\">\\(.*[^ ]\\)<\\/h[0-9]>" ; this is quite restrictive, but due to `org-reference-contraction' I can do this "<h\\1 id=\"\\2\">\\3<a aria-hidden=\"true\" href=\"#\\2\">#</a> </h\\1>" text)))) (add-to-list 'org-export-filter-headline-functions 'org-export-html-headline-anchor) (org-link-set-parameters "Https" :follow (lambda (url arg) (browse-url (concat "https:" url) arg)) :export #'org-url-fancy-export) (defun org-url-fancy-export (url _desc backend) (let ((metadata (org-url-unfurl-metadata (concat "https:" url)))) (cond ((org-export-derived-backend-p backend 'html) (concat "<div class=\"link-preview\">" (format "<a href=\"%s\">" (concat "https:" url)) (when (plist-get metadata :image) (format "<img src=\"%s\"/>" (plist-get metadata :image))) "<small>" (replace-regexp-in-string "//\\(?:www\\.\\)?\\([^/]+\\)/?.*" "\\1" url) "</small><p>" (when (plist-get metadata :title) (concat "<b>" (org-html-encode-plain-text (plist-get metadata :title)) "</b></br>")) (when (plist-get metadata :description) (org-html-encode-plain-text (plist-get metadata :description))) "</p></a></div>")) (t url)))) (setq org-url-unfurl-metadata--cache nil) (defun org-url-unfurl-metadata (url) (cdr (or (assoc url org-url-unfurl-metadata--cache) (car (push (cons url (let* ((head-data (-filter #'listp (cdaddr (with-current-buffer (progn (message "Fetching metadata from %s" url) (url-retrieve-synchronously url t t 5)) (goto-char (point-min)) (delete-region (point-min) (- (search-forward "<head") 6)) (delete-region (search-forward "</head>") (point-max)) (goto-char (point-min)) (while (re-search-forward "<script[^\u2800]+?</script>" nil t) (replace-match "")) (goto-char (point-min)) (while (re-search-forward "<style[^\u2800]+?</style>" nil t) (replace-match "")) (libxml-parse-html-region (point-min) (point-max)))))) (meta (delq nil (mapcar (lambda (tag) (when (eq 'meta (car tag)) (cons (or (cdr (assoc 'name (cadr tag))) (cdr (assoc 'property (cadr tag)))) (cdr (assoc 'content (cadr tag)))))) head-data)))) (let ((title (or (cdr (assoc "og:title" meta)) (cdr (assoc "twitter:title" meta)) (nth 2 (assq 'title head-data)))) (description (or (cdr (assoc "og:description" meta)) (cdr (assoc "twitter:description" meta)) (cdr (assoc "description" meta)))) (image (or (cdr (assoc "og:image" meta)) (cdr (assoc "twitter:image" meta))))) (when image (setq image (replace-regexp-in-string "^/" (concat "https://" (replace-regexp-in-string "//\\([^/]+\\)/?.*" "\\1" url) "/") (replace-regexp-in-string "^//" "https://" image)))) (list :title title :description description :image image)))) org-url-unfurl-metadata--cache))))) (setq org-html-mathjax-options '((path "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js" ) (scale "1") (autonumber "ams") (multlinewidth "85%") (tagindent ".8em") (tagside "right"))) (setq org-html-mathjax-template "<script> MathJax = { chtml: { scale: %SCALE }, svg: { scale: %SCALE, fontCache: \"global\" }, tex: { tags: \"%AUTONUMBER\", multlineWidth: \"%MULTLINEWIDTH\", tagSide: \"%TAGSIDE\", tagIndent: \"%TAGINDENT\" } }; </script> <script id=\"MathJax-script\" async src=\"%PATH\"></script>") )
There are quite a few instances where I want to modify variables defined in ox-html, so we’ll wrap the contents of this section in a
Tecosaur has a good collection of fonts, might as well take some
HTML
<link rel="icon" href="https://tecosaur.com/resources/org/nib.ico" type="image/ico" /> <link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="https://tecosaur.com/resources/org/etbookot-roman-webfont.woff2"> <link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="https://tecosaur.com/resources/org/etbookot-italic-webfont.woff2"> <link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="https://tecosaur.com/resources/org/Merriweather-TextRegular.woff2"> <link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="https://tecosaur.com/resources/org/Merriweather-TextItalic.woff2"> <link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="https://tecosaur.com/resources/org/Merriweather-TextBold.woff2">
Example, fixed width, and property blocksEmacs Lisp
(defun org-html-block-collapsable (orig-fn block contents info) "Wrap the usual block in a <details>" (if (or (not org-fancy-html-export-mode) (bound-and-true-p org-msg-export-in-progress)) (funcall orig-fn block contents info) (let ((ref (org-export-get-reference block info)) (type (pcase (car block) ('property-drawer "Properties"))) (collapsed-default (pcase (car block) ('property-drawer t) (_ nil))) (collapsed-value (org-export-read-attribute :attr_html block :collapsed)) (collapsed-p (or (member (org-export-read-attribute :attr_html block :collapsed) '("y" "yes" "t" t "true")) (member (plist-get info :collapsed) '("all"))))) (format "<details id='%s' class='code'%s> <summary%s>%s</summary> <div class='gutter'>\ <a href='#%s'>#</a> <button title='Copy to clipboard' onclick='copyPreToClipbord(this)'>⎘</button>\ </div> %s\n </details>" ref (if (or collapsed-p collapsed-default) "" " open") (if type " class='named'" "") (if type (format "<span class='type'>%s</span>" type) "") ref (funcall orig-fn block contents info))))) (advice-add 'org-html-example-block :around #'org-html-block-collapsable) (advice-add 'org-html-fixed-width :around #'org-html-block-collapsable) (advice-add 'org-html-property-drawer :around #'org-html-block-collapsable)
- HTML
- Org-Roam
I would like to get into the habit of using org-roam for my notes, mainly because of that cool reddit post with the server.
Lets set up the org-roam-ui as well
elisp
(use-package! websocket :after org-roam) (use-package! org-roam-ui :after org-roam :commands org-roam-ui-open :config (setq org-roam-ui-sync-theme t org-roam-ui-follow t org-roam-ui-update-on-save t org-roam-ui-open-on-start t))
The doom-modeline is a bit messy with roam, lets adjust that
elisp
(defadvice! doom-modeline--buffer-file-name-roam-aware-a (orig-fun) :around #'doom-modeline-buffer-file-name ; takes no args (if (s-contains-p org-roam-directory (or buffer-file-name "")) (replace-regexp-in-string "\\(?:^\\|.*/\\)\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)[0-9]*-" "🢔(\\1-\\2-\\3) " (subst-char-in-string ?_ ? buffer-file-name)) (funcall orig-fun)))
Now, I want to replace the org-roam buffer with org-roam-ui, to do that, we need to disable the regular buffer
- Org-Agenda
Set the directory
- Org Cite
elisp
(use-package! citar :when (featurep! :completion vertico) :no-require :config (setq org-cite-insert-processor 'citar org-cite-follow-processor 'citar org-cite-activate-processor 'citar citar-bibliography '("~/org/references.bib")) (when (featurep! :lang org +roam2) ;; Include property drawer metadata for 'org-roam' v2. (setq citar-file-note-org-include '(org-id org-roam-ref)))) (use-package! citar :when (featurep! :completion vertico) :after org) (use-package! citeproc :defer t) ;;; Org-Cite configuration (map! :after org :map org-mode-map :localleader :desc "Insert citation" "@" #'org-cite-insert) (use-package! oc :after citar :config (require 'ox) (setq org-cite-global-bibliography (let ((paths (or citar-bibliography (bound-and-true-p bibtex-completion-bibliography)))) ;; Always return bibliography paths as list for org-cite. (if (stringp paths) (list paths) paths))) ;; setup export processor; default csl/citeproc-el, with biblatex for latex (setq org-cite-export-processors '((t csl)))) ;;; Org-cite processors (use-package! oc-biblatex :after oc) (use-package! oc-csl :after oc :config (setq org-cite-csl-styles-dir "~/.config/bib/styles")) (use-package! oc-natbib :after oc) ;;;; Third-party (use-package! citar-org :no-require :custom (org-cite-insert-processor 'citar) (org-cite-follow-processor 'citar) (org-cite-activate-processor 'citar) (org-support-shift-select t) (citar-bibliography '("~/org/references.bib")) (when (featurep! :lang org +roam2) ;; Include property drawer metadata for 'org-roam' v2. (citar-org-note-include '(org-id org-roam-ref))) ;; Personal extras (setq citar-symbols `((file ,(all-the-icons-faicon "file-o" :v-adjust -0.1) . " ") (note ,(all-the-icons-material "speaker_notes" :face 'all-the-icons-silver :v-adjust -0.3) . " ") (link ,(all-the-icons-octicon "link" :face 'all-the-icons-dsilver :v-adjust 0.01) . " ")))) (use-package! oc-csl-activate :after oc :config (setq org-cite-csl-activate-use-document-style t) (defun +org-cite-csl-activate/enable () (interactive) (setq org-cite-activate-processor 'csl-activate) (add-hook! 'org-mode-hook '((lambda () (cursor-sensor-mode 1)) org-cite-csl-activate-render-all)) (defadvice! +org-cite-csl-activate-render-all-silent (orig-fn) :around #'org-cite-csl-activate-render-all (with-silent-modifications (funcall orig-fn))) (when (eq major-mode 'org-mode) (with-silent-modifications (save-excursion (goto-char (point-min)) (org-cite-activate (point-max))) (org-cite-csl-activate-render-all))) (fmakunbound #'+org-cite-csl-activate/enable)))
- Org-Capture
Use doct
- Prettify
Improve the look of the capture dialog (idea borrowed from tecosaur)
Emacs Lisp
(defun org-capture-select-template-prettier (&optional keys) "Select a capture template, in a prettier way than default Lisp programs can force the template by setting KEYS to a string." (let ((org-capture-templates (or (org-contextualize-keys (org-capture-upgrade-templates org-capture-templates) org-capture-templates-contexts) '(("t" "Task" entry (file+headline "" "Tasks") "* TODO %?\n %u\n %a"))))) (if keys (or (assoc keys org-capture-templates) (error "No capture template referred to by \"%s\" keys" keys)) (org-mks org-capture-templates "Select a capture template\n━━━━━━━━━━━━━━━━━━━━━━━━━" "Template key: " `(("q" ,(concat (all-the-icons-octicon "stop" :face 'all-the-icons-red :v-adjust 0.01) "\tAbort"))))))) (advice-add 'org-capture-select-template :override #'org-capture-select-template-prettier) (defun org-mks-pretty (table title &optional prompt specials) "Select a member of an alist with multiple keys. Prettified. TABLE is the alist which should contain entries where the car is a string. There should be two types of entries. 1. prefix descriptions like (\"a\" \"Description\") This indicates that `a' is a prefix key for multi-letter selection, and that there are entries following with keys like \"ab\", \"ax\"… 2. Select-able members must have more than two elements, with the first being the string of keys that lead to selecting it, and the second a short description string of the item. The command will then make a temporary buffer listing all entries that can be selected with a single key, and all the single key prefixes. When you press the key for a single-letter entry, it is selected. When you press a prefix key, the commands (and maybe further prefixes) under this key will be shown and offered for selection. TITLE will be placed over the selection in the temporary buffer, PROMPT will be used when prompting for a key. SPECIALS is an alist with (\"key\" \"description\") entries. When one of these is selected, only the bare key is returned." (save-window-excursion (let ((inhibit-quit t) (buffer (org-switch-to-buffer-other-window "*Org Select*")) (prompt (or prompt "Select: ")) case-fold-search current) (unwind-protect (catch 'exit (while t (setq-local evil-normal-state-cursor (list nil)) (erase-buffer) (insert title "\n\n") (let ((des-keys nil) (allowed-keys '("\C-g")) (tab-alternatives '("\s" "\t" "\r")) (cursor-type nil)) ;; Populate allowed keys and descriptions keys ;; available with CURRENT selector. (let ((re (format "\\`%s\\(.\\)\\'" (if current (regexp-quote current) ""))) (prefix (if current (concat current " ") ""))) (dolist (entry table) (pcase entry ;; Description. (`(,(and key (pred (string-match re))) ,desc) (let ((k (match-string 1 key))) (push k des-keys) ;; Keys ending in tab, space or RET are equivalent. (if (member k tab-alternatives) (push "\t" allowed-keys) (push k allowed-keys)) (insert (propertize prefix 'face 'font-lock-comment-face) (propertize k 'face 'bold) (propertize "›" 'face 'font-lock-comment-face) " " desc "…" "\n"))) ;; Usable entry. (`(,(and key (pred (string-match re))) ,desc . ,_) (let ((k (match-string 1 key))) (insert (propertize prefix 'face 'font-lock-comment-face) (propertize k 'face 'bold) " " desc "\n") (push k allowed-keys))) (_ nil)))) ;; Insert special entries, if any. (when specials (insert "─────────────────────────\n") (pcase-dolist (`(,key ,description) specials) (insert (format "%s %s\n" (propertize key 'face '(bold all-the-icons-red)) description)) (push key allowed-keys))) ;; Display UI and let user select an entry or ;; a sub-level prefix. (goto-char (point-min)) (unless (pos-visible-in-window-p (point-max)) (org-fit-window-to-buffer)) (let ((pressed (org--mks-read-key allowed-keys prompt nil))) (setq current (concat current pressed)) (cond ((equal pressed "\C-g") (user-error "Abort")) ((equal pressed "ESC") (user-error "Abort")) ;; Selection is a prefix: open a new menu. ((member pressed des-keys)) ;; Selection matches an association: return it. ((let ((entry (assoc current table))) (and entry (throw 'exit entry)))) ;; Selection matches a special entry: return the ;; selection prefix. ((assoc current specials) (throw 'exit current)) (t (error "No entry available"))))))) (when buffer (kill-buffer buffer)))))) (advice-add 'org-mks :override #'org-mks-pretty)
The org-capture bin is rather nice, but I’d be nicer with a smaller frame, and no modeline.
Emacs Lisp
(setf (alist-get 'height +org-capture-frame-parameters) 15) (setq +org-capture-fn (lambda () (interactive) (set-window-parameter nil 'mode-line-format 'none) (org-capture)))
Sprinkle in some doct utility functions
Emacs Lisp
(defun +doct-icon-declaration-to-icon (declaration) "Convert :icon declaration to icon" (let ((name (pop declaration)) (set (intern (concat "all-the-icons-" (plist-get declaration :set)))) (face (intern (concat "all-the-icons-" (plist-get declaration :color)))) (v-adjust (or (plist-get declaration :v-adjust) 0.01))) (apply set `(,name :face ,face :v-adjust ,v-adjust)))) (defun +doct-iconify-capture-templates (groups) "Add declaration's :icon to each template group in GROUPS." (let ((templates (doct-flatten-lists-in groups))) (setq doct-templates (mapcar (lambda (template) (when-let* ((props (nthcdr (if (= (length template) 4) 2 5) template)) (spec (plist-get (plist-get props :doct) :icon))) (setf (nth 1 template) (concat (+doct-icon-declaration-to-icon spec) "\t" (nth 1 template)))) template) templates)))) (setq doct-after-conversion-functions '(+doct-iconify-capture-templates))
- Templates
Emacs Lisp
(setq org-capture-templates (doct `(("Home" :keys "h" :icon ("home" :set "octicon" :color "cyan") :file "Home.org" :prepend t :headline "Inbox" :template ("* TODO %?" "%i %a")) ("Work" :keys "w" :icon ("business" :set "material" :color "yellow") :file "Work.org" :prepend t :headline "Inbox" :template ("* TODO %?" "SCHEDULED: %^{Schedule:}t" "DEADLINE: %^{Deadline:}t" "%i %a")) ("Note" :keys "n" :icon ("sticky-note" :set "faicon" :color "yellow") :file "Notes.org" :template ("* *?" "%i %a")) ("Project" :keys "p" :icon ("repo" :set "octicon" :color "silver") :prepend t :type entry :headline "Inbox" :template ("* %{keyword} %?" "%i" "%a") :file "" :custom (:keyword "") :children (("Task" :keys "t" :icon ("checklist" :set "octicon" :color "green") :keyword "TODO" :file +org-capture-project-todo-file) ("Note" :keys "n" :icon ("sticky-note" :set "faicon" :color "yellow") :keyword "%U" :file +org-capture-project-notes-file))) )))
- Prettify
- Org-Plot
You can’t ever have too many graphs! Lets make it look prettier, and tell it to use the doom theme colors
elisp
(after! org-plot (defun org-plot/generate-theme (_type) "Use the current Doom theme colours to generate a GnuPlot preamble." (format " fgt = \"textcolor rgb '%s'\" # foreground text fgat = \"textcolor rgb '%s'\" # foreground alt text fgl = \"linecolor rgb '%s'\" # foreground line fgal = \"linecolor rgb '%s'\" # foreground alt line # foreground colors set border lc rgb '%s' # change text colors of tics set xtics @fgt set ytics @fgt # change text colors of labels set title @fgt set xlabel @fgt set ylabel @fgt # change a text color of key set key @fgt # line styles set linetype 1 lw 2 lc rgb '%s' # red set linetype 2 lw 2 lc rgb '%s' # blue set linetype 3 lw 2 lc rgb '%s' # green set linetype 4 lw 2 lc rgb '%s' # magenta set linetype 5 lw 2 lc rgb '%s' # orange set linetype 6 lw 2 lc rgb '%s' # yellow set linetype 7 lw 2 lc rgb '%s' # teal set linetype 8 lw 2 lc rgb '%s' # violet # border styles set tics out nomirror set border 3 # palette set palette maxcolors 8 set palette defined ( 0 '%s',\ 1 '%s',\ 2 '%s',\ 3 '%s',\ 4 '%s',\ 5 '%s',\ 6 '%s',\ 7 '%s' ) " (doom-color 'fg) (doom-color 'fg-alt) (doom-color 'fg) (doom-color 'fg-alt) (doom-color 'fg) ;; colours (doom-color 'red) (doom-color 'blue) (doom-color 'green) (doom-color 'magenta) (doom-color 'orange) (doom-color 'yellow) (doom-color 'teal) (doom-color 'violet) ;; duplicated (doom-color 'red) (doom-color 'blue) (doom-color 'green) (doom-color 'magenta) (doom-color 'orange) (doom-color 'yellow) (doom-color 'teal) (doom-color 'violet) )) (defun org-plot/gnuplot-term-properties (_type) (format "background rgb '%s' size 1050,650" (doom-color 'bg))) (setq org-plot/gnuplot-script-preamble #'org-plot/generate-theme) (setq org-plot/gnuplot-term-extra #'org-plot/gnuplot-term-properties))
- XKCD
Relevent XKCD:
I link to xkcd’s so much that its better to just have a configuration for them We want to set this up so it loads nicely in org.
Emacs Lisp
(use-package! xkcd :commands (xkcd-get-json xkcd-download xkcd-get ;; now for funcs from my extension of this pkg +xkcd-find-and-copy +xkcd-find-and-view +xkcd-fetch-info +xkcd-select) :config (setq xkcd-cache-dir (expand-file-name "xkcd/" doom-cache-dir) xkcd-cache-latest (concat xkcd-cache-dir "latest")) (unless (file-exists-p xkcd-cache-dir) (make-directory xkcd-cache-dir)) (after! evil-snipe (add-to-list 'evil-snipe-disabled-modes 'xkcd-mode)) :general (:states 'normal :keymaps 'xkcd-mode-map "<right>" #'xkcd-next "n" #'xkcd-next ; evil-ish "<left>" #'xkcd-prev "N" #'xkcd-prev ; evil-ish "r" #'xkcd-rand "a" #'xkcd-rand ; because image-rotate can interfere "t" #'xkcd-alt-text "q" #'xkcd-kill-buffer "o" #'xkcd-open-browser "e" #'xkcd-open-explanation-browser ;; extras "s" #'+xkcd-find-and-view "/" #'+xkcd-find-and-view "y" #'+xkcd-copy))
Let’s also extend the functionality a whole bunch.
Emacs Lisp
(after! xkcd (require 'emacsql-sqlite) (defun +xkcd-select () "Prompt the user for an xkcd using `completing-read' and `+xkcd-select-format'. Return the xkcd number or nil" (let* (prompt-lines (-dummy (maphash (lambda (key xkcd-info) (push (+xkcd-select-format xkcd-info) prompt-lines)) +xkcd-stored-info)) (num (completing-read (format "xkcd (%s): " xkcd-latest) prompt-lines))) (if (equal "" num) xkcd-latest (string-to-number (replace-regexp-in-string "\\([0-9]+\\).*" "\\1" num))))) (defun +xkcd-select-format (xkcd-info) "Creates each completing-read line from an xkcd info plist. Must start with the xkcd number" (format "%-4s %-30s %s" (propertize (number-to-string (plist-get xkcd-info :num)) 'face 'counsel-key-binding) (plist-get xkcd-info :title) (propertize (plist-get xkcd-info :alt) 'face '(variable-pitch font-lock-comment-face)))) (defun +xkcd-fetch-info (&optional num) "Fetch the parsed json info for comic NUM. Fetches latest when omitted or 0" (require 'xkcd) (when (or (not num) (= num 0)) (+xkcd-check-latest) (setq num xkcd-latest)) (let ((res (or (gethash num +xkcd-stored-info) (puthash num (+xkcd-db-read num) +xkcd-stored-info)))) (unless res (+xkcd-db-write (let* ((url (format "https://xkcd.com/%d/info.0.json" num)) (json-assoc (if (gethash num +xkcd-stored-info) (gethash num +xkcd-stored-info) (json-read-from-string (xkcd-get-json url num))))) json-assoc)) (setq res (+xkcd-db-read num))) res)) ;; since we've done this, we may as well go one little step further (defun +xkcd-find-and-copy () "Prompt for an xkcd using `+xkcd-select' and copy url to clipboard" (interactive) (+xkcd-copy (+xkcd-select))) (defun +xkcd-copy (&optional num) "Copy a url to xkcd NUM to the clipboard" (interactive "i") (let ((num (or num xkcd-cur))) (gui-select-text (format "https://xkcd.com/%d" num)) (message "xkcd.com/%d copied to clipboard" num))) (defun +xkcd-find-and-view () "Prompt for an xkcd using `+xkcd-select' and view it" (interactive) (xkcd-get (+xkcd-select)) (switch-to-buffer "*xkcd*")) (defvar +xkcd-latest-max-age (* 60 60) ; 1 hour "Time after which xkcd-latest should be refreshed, in seconds") ;; initialise `xkcd-latest' and `+xkcd-stored-info' with latest xkcd (add-transient-hook! '+xkcd-select (require 'xkcd) (+xkcd-fetch-info xkcd-latest) (setq +xkcd-stored-info (+xkcd-db-read-all))) (add-transient-hook! '+xkcd-fetch-info (xkcd-update-latest)) (defun +xkcd-check-latest () "Use value in `xkcd-cache-latest' as long as it isn't older thabn `+xkcd-latest-max-age'" (unless (and (file-exists-p xkcd-cache-latest) (< (- (time-to-seconds (current-time)) (time-to-seconds (file-attribute-modification-time (file-attributes xkcd-cache-latest)))) +xkcd-latest-max-age)) (let* ((out (xkcd-get-json "http://xkcd.com/info.0.json" 0)) (json-assoc (json-read-from-string out)) (latest (cdr (assoc 'num json-assoc)))) (when (/= xkcd-latest latest) (+xkcd-db-write json-assoc) (with-current-buffer (find-file xkcd-cache-latest) (setq xkcd-latest latest) (erase-buffer) (insert (number-to-string latest)) (save-buffer) (kill-buffer (current-buffer))))) (shell-command (format "touch %s" xkcd-cache-latest)))) (defvar +xkcd-stored-info (make-hash-table :test 'eql) "Basic info on downloaded xkcds, in the form of a hashtable") (defadvice! xkcd-get-json--and-cache (url &optional num) "Fetch the Json coming from URL. If the file NUM.json exists, use it instead. If NUM is 0, always download from URL. The return value is a string." :override #'xkcd-get-json (let* ((file (format "%s%d.json" xkcd-cache-dir num)) (cached (and (file-exists-p file) (not (eq num 0)))) (out (with-current-buffer (if cached (find-file file) (url-retrieve-synchronously url)) (goto-char (point-min)) (unless cached (re-search-forward "^$")) (prog1 (buffer-substring-no-properties (point) (point-max)) (kill-buffer (current-buffer)))))) (unless (or cached (eq num 0)) (xkcd-cache-json num out)) out)) (defadvice! +xkcd-get (num) "Get the xkcd number NUM." :override 'xkcd-get (interactive "nEnter comic number: ") (xkcd-update-latest) (get-buffer-create "*xkcd*") (switch-to-buffer "*xkcd*") (xkcd-mode) (let (buffer-read-only) (erase-buffer) (setq xkcd-cur num) (let* ((xkcd-data (+xkcd-fetch-info num)) (num (plist-get xkcd-data :num)) (img (plist-get xkcd-data :img)) (safe-title (plist-get xkcd-data :safe-title)) (alt (plist-get xkcd-data :alt)) title file) (message "Getting comic...") (setq file (xkcd-download img num)) (setq title (format "%d: %s" num safe-title)) (insert (propertize title 'face 'outline-1)) (center-line) (insert "\n") (xkcd-insert-image file num) (if (eq xkcd-cur 0) (setq xkcd-cur num)) (setq xkcd-alt alt) (message "%s" title)))) (defconst +xkcd-db--sqlite-available-p (with-demoted-errors "+org-xkcd initialization: %S" (emacsql-sqlite-ensure-binary) t)) (defvar +xkcd-db--connection (make-hash-table :test #'equal) "Database connection to +org-xkcd database.") (defun +xkcd-db--get () "Return the sqlite db file." (expand-file-name "xkcd.db" xkcd-cache-dir)) (defun +xkcd-db--get-connection () "Return the database connection, if any." (gethash (file-truename xkcd-cache-dir) +xkcd-db--connection)) (defconst +xkcd-db--table-schema '((xkcds [(num integer :unique :primary-key) (year :not-null) (month :not-null) (link :not-null) (news :not-null) (safe_title :not-null) (title :not-null) (transcript :not-null) (alt :not-null) (img :not-null)]))) (defun +xkcd-db--init (db) "Initialize database DB with the correct schema and user version." (emacsql-with-transaction db (pcase-dolist (`(,table . ,schema) +xkcd-db--table-schema) (emacsql db [:create-table $i1 $S2] table schema)))) (defun +xkcd-db () "Entrypoint to the +org-xkcd sqlite database. Initializes and stores the database, and the database connection. Performs a database upgrade when required." (unless (and (+xkcd-db--get-connection) (emacsql-live-p (+xkcd-db--get-connection))) (let* ((db-file (+xkcd-db--get)) (init-db (not (file-exists-p db-file)))) (make-directory (file-name-directory db-file) t) (let ((conn (emacsql-sqlite db-file))) (set-process-query-on-exit-flag (emacsql-process conn) nil) (puthash (file-truename xkcd-cache-dir) conn +xkcd-db--connection) (when init-db (+xkcd-db--init conn))))) (+xkcd-db--get-connection)) (defun +xkcd-db-query (sql &rest args) "Run SQL query on +org-xkcd database with ARGS. SQL can be either the emacsql vector representation, or a string." (if (stringp sql) (emacsql (+xkcd-db) (apply #'format sql args)) (apply #'emacsql (+xkcd-db) sql args))) (defun +xkcd-db-read (num) (when-let ((res (car (+xkcd-db-query [:select * :from xkcds :where (= num $s1)] num :limit 1)))) (+xkcd-db-list-to-plist res))) (defun +xkcd-db-read-all () (let ((xkcd-table (make-hash-table :test 'eql :size 4000))) (mapcar (lambda (xkcd-info-list) (puthash (car xkcd-info-list) (+xkcd-db-list-to-plist xkcd-info-list) xkcd-table)) (+xkcd-db-query [:select * :from xkcds])) xkcd-table)) (defun +xkcd-db-list-to-plist (xkcd-datalist) `(:num ,(nth 0 xkcd-datalist) :year ,(nth 1 xkcd-datalist) :month ,(nth 2 xkcd-datalist) :link ,(nth 3 xkcd-datalist) :news ,(nth 4 xkcd-datalist) :safe-title ,(nth 5 xkcd-datalist) :title ,(nth 6 xkcd-datalist) :transcript ,(nth 7 xkcd-datalist) :alt ,(nth 8 xkcd-datalist) :img ,(nth 9 xkcd-datalist))) (defun +xkcd-db-write (data) (+xkcd-db-query [:insert-into xkcds :values $v1] (list (vector (cdr (assoc 'num data)) (cdr (assoc 'year data)) (cdr (assoc 'month data)) (cdr (assoc 'link data)) (cdr (assoc 'news data)) (cdr (assoc 'safe_title data)) (cdr (assoc 'title data)) (cdr (assoc 'transcript data)) (cdr (assoc 'alt data)) (cdr (assoc 'img data)) )))))
Now to just have this register with org
Emacs Lisp
(after! org (org-link-set-parameters "xkcd" :image-data-fun #'+org-xkcd-image-fn :follow #'+org-xkcd-open-fn :export #'+org-xkcd-export :complete #'+org-xkcd-complete) (defun +org-xkcd-open-fn (link) (+org-xkcd-image-fn nil link nil)) (defun +org-xkcd-image-fn (protocol link description) "Get image data for xkcd num LINK" (let* ((xkcd-info (+xkcd-fetch-info (string-to-number link))) (img (plist-get xkcd-info :img)) (alt (plist-get xkcd-info :alt))) (message alt) (+org-image-file-data-fn protocol (xkcd-download img (string-to-number link)) description))) (defun +org-xkcd-export (num desc backend _com) "Convert xkcd to html/LaTeX form" (let* ((xkcd-info (+xkcd-fetch-info (string-to-number num))) (img (plist-get xkcd-info :img)) (alt (plist-get xkcd-info :alt)) (title (plist-get xkcd-info :title)) (file (xkcd-download img (string-to-number num)))) (cond ((org-export-derived-backend-p backend 'html) (format "<img class='invertible' src='%s' title=\"%s\" alt='%s'>" img (subst-char-in-string ?\" ?“ alt) title)) ((org-export-derived-backend-p backend 'latex) (format "\\begin{figure}[!htb] \\centering \\includegraphics[scale=0.4]{%s}%s \\end{figure}" file (if (equal desc (format "xkcd:%s" num)) "" (format "\n \\caption*{\\label{xkcd:%s} %s}" num (or desc (format "\\textbf{%s} %s" title alt)))))) (t (format "https://xkcd.com/%s" num))))) (defun +org-xkcd-complete (&optional arg) "Complete xkcd using `+xkcd-stored-info'" (format "xkcd:%d" (+xkcd-select))))
- View Exported File
I have to export files pretty often, lets setup some keybindings to make it easier
elisp
(map! :map org-mode-map :localleader :desc "View exported file" "v" #'org-view-output-file) (defun org-view-output-file (&optional org-file-path) "Visit buffer open on the first output file (if any) found, using `org-view-output-file-extensions'" (interactive) (let* ((org-file-path (or org-file-path (buffer-file-name) "")) (dir (file-name-directory org-file-path)) (basename (file-name-base org-file-path)) (output-file nil)) (dolist (ext org-view-output-file-extensions) (unless output-file (when (file-exists-p (concat dir basename "." ext)) (setq output-file (concat dir basename "." ext))))) (if output-file (if (member (file-name-extension output-file) org-view-external-file-extensions) (browse-url-xdg-open output-file) (pop-to-bufferpop-to-buffer (or (find-buffer-visiting output-file) (find-file-noselect output-file)))) (message "No exported file found")))) (defvar org-view-output-file-extensions '("pdf" "md" "rst" "txt" "tex" "html") "Search for output files with these extensions, in order, viewing the first that matches") (defvar org-view-external-file-extensions '("html") "File formats that should be opened externally.")
- Dictionaries
Lets use lexic instead of the default dictionary
elisp
(use-package! lexic :commands lexic-search lexic-list-dictionary :config (map! :map lexic-mode-map :n "q" #'lexic-return-from-lexic :nv "RET" #'lexic-search-word-at-point :n "a" #'outline-show-all :n "h" (cmd! (outline-hide-sublevels 3)) :n "o" #'lexic-toggle-entry :n "n" #'lexic-next-entry :n "N" (cmd! (lexic-next-entry t)) :n "p" #'lexic-previous-entry :n "P" (cmd! (lexic-previous-entry t)) :n "E" (cmd! (lexic-return-from-lexic) ; expand (switch-to-buffer (lexic-get-buffer))) :n "M" (cmd! (lexic-return-from-lexic) ; minimise (lexic-goto-lexic)) :n "C-p" #'lexic-search-history-backwards :n "C-n" #'lexic-search-history-forwards :n "/" (cmd! (call-interactively #'lexic-search)))) (defadvice! +lookup/dictionary-definition-lexic (identifier &optional arg) "Look up the definition of the word at point (or selection) using `lexic-search'." :override #'+lookup/dictionary-definition (interactive (list (or (doom-thing-at-point-or-region 'word) (read-string "Look up in dictionary: ")) current-prefix-arg)) (lexic-search identifier nil nil t))
5.1.7. Latex
I have a love-hate relationship with latex. Its extremely powerful, but at the same time its hard to write, hard to understand, and very slow. The solution: write everything in org and then export it to tex. Best of both worlds!
- Basic configuration
First of all, lets use pdf-tools to preview pdfs by defaults
I also want to adjust the look of those previews
elisp
(after! org (setq org-highlight-latex-and-related '(native script entities)) (add-to-list 'org-src-block-faces '("latex" (:inherit default :extend t)))) (after! org (plist-put org-format-latex-options :background "Transparent"))
Lets add cdlatex org mode integration
elisp
(after! org (add-hook 'org-mode-hook 'turn-on-org-cdlatex)) (defadvice! org-edit-latex-emv-after-insert () :after #'org-cdlatex-environment-indent (org-edit-latex-environment))
I like to preview images inline too
elisp
(setq org-display-inline-images t) (setq org-redisplay-inline-images t) (setq org-startup-with-inline-images "inlineimages")
Obviously we can’t edit a png though. Let use org-fragtog to toggle between previews and text mode
Here’s just my private LaTeX config.
elisp
(setq org-format-latex-header "\\documentclass{article} \\usepackage[usenames]{xcolor} \\usepackage[T1]{fontenc} \\usepackage{booktabs} \\pagestyle{empty} % do not remove % The settings below are copied from fullpage.sty \\setlength{\\textwidth}{\\paperwidth} \\addtolength{\\textwidth}{-3cm} \\setlength{\\oddsidemargin}{1.5cm} \\addtolength{\\oddsidemargin}{-2.54cm} \\setlength{\\evensidemargin}{\\oddsidemargin} \\setlength{\\textheight}{\\paperheight} \\addtolength{\\textheight}{-\\headheight} \\addtolength{\\textheight}{-\\headsep} \\addtolength{\\textheight}{-\\footskip} \\addtolength{\\textheight}{-3cm} \\setlength{\\topmargin}{1.5cm} \\addtolength{\\topmargin}{-2.54cm} ")
- PDF-Tools
DocView gives me a headache, but pdf-tools can be improved, lets configure it a little more
elisp
(use-package pdf-view :hook (pdf-tools-enabled . pdf-view-themed-minor-mode) :hook (pdf-tools-enabled . hide-mode-line-mode) :config (setq pdf-view-resize-factor 1.1) (setq-default pdf-view-display-size 'fit-page))
- Export
- Conditional features
Emacs Lisp
(defvar org-latex-italic-quotes t "Make \"quote\" environments italic.") (defvar org-latex-par-sep t "Vertically seperate paragraphs, and remove indentation.") (defvar org-latex-conditional-features '(("\\[\\[\\(?:file\\|https?\\):\\(?:[^]]\\|\\\\\\]\\)+?\\.\\(?:eps\\|pdf\\|png\\|jpeg\\|jpg\\|jbig2\\)\\]\\]" . image) ("\\[\\[\\(?:file\\|https?\\):\\(?:[^]]+?\\|\\\\\\]\\)\\.svg\\]\\]" . svg) ("^[ \t]*|" . table) ("cref:\\|\\cref{\\|\\[\\[[^\\]]+\\]\\]" . cleveref) ("[;\\\\]?\\b[A-Z][A-Z]+s?[^A-Za-z]" . acronym) ("\\+[^ ].*[^ ]\\+\\|_[^ ].*[^ ]_\\|\\\\uu?line\\|\\\\uwave\\|\\\\sout\\|\\\\xout\\|\\\\dashuline\\|\\dotuline\\|\\markoverwith" . underline) (":float wrap" . float-wrap) (":float sideways" . rotate) ("^[ \t]*#\\+caption:\\|\\\\caption" . caption) ("\\[\\[xkcd:" . (image caption)) ((and org-latex-italic-quotes "^[ \t]*#\\+begin_quote\\|\\\\begin{quote}") . italic-quotes) (org-latex-par-sep . par-sep) ("^[ \t]*\\(?:[-+*]\\|[0-9]+[.)]\\|[A-Za-z]+[.)]\\) \\[[ -X]\\]" . checkbox) ("^[ \t]*#\\+begin_warning\\|\\\\begin{warning}" . box-warning) ("^[ \t]*#\\+begin_info\\|\\\\begin{info}" . box-info) ("^[ \t]*#\\+begin_success\\|\\\\begin{success}" . box-success) ("^[ \t]*#\\+begin_error\\|\\\\begin{error}" . box-error)) "Org feature tests and associated LaTeX feature flags. Alist where the car is a test for the presense of the feature, and the cdr is either a single feature symbol or list of feature symbols. When a string, it is used as a regex search in the buffer. The feature is registered as present when there is a match. The car can also be a - symbol, the value of which is fetched - function, which is called with info as an argument - list, which is `eval'uated If the symbol, function, or list produces a string: that is used as a regex search in the buffer. Otherwise any non-nil return value will indicate the existance of the feature.")
Emacs Lisp
(defvar org-latex-caption-preamble " \\usepackage{subcaption} \\usepackage[hypcap=true]{caption} \\setkomafont{caption}{\\sffamily\\small} \\setkomafont{captionlabel}{\\upshape\\bfseries} \\captionsetup{justification=raggedright,singlelinecheck=true} \\usepackage{capt-of} % required by Org " "Preamble that improves captions.") (defvar org-latex-checkbox-preamble " \\newcommand{\\checkboxUnchecked}{$\\square$} \\newcommand{\\checkboxTransitive}{\\rlap{\\raisebox{-0.1ex}{\\hspace{0.35ex}\\Large\\textbf -}}$\\square$} \\newcommand{\\checkboxChecked}{\\rlap{\\raisebox{0.2ex}{\\hspace{0.35ex}\\scriptsize \\ding{52}}}$\\square$} " "Preamble that improves checkboxes.") (defvar org-latex-box-preamble " % args = #1 Name, #2 Colour, #3 Ding, #4 Label \\newcommand{\\defsimplebox}[4]{% \\definecolor{#1}{HTML}{#2} \\newenvironment{#1}[1][] {% \\par\\vspace{-0.7\\baselineskip}% \\textcolor{#1}{#3} \\textcolor{#1}{\\textbf{\\def\\temp{##1}\\ifx\\temp\\empty#4\\else##1\\fi}}% \\vspace{-0.8\\baselineskip} \\begin{addmargin}[1em]{1em} }{% \\end{addmargin} \\vspace{-0.5\\baselineskip} }% } " "Preamble that provides a macro for custom boxes.")
Emacs Lisp
(defvar org-latex-feature-implementations '((image :snippet "\\usepackage{graphicx}" :order 2) (svg :snippet "\\usepackage{svg}" :order 2) (table :snippet "\\usepackage{longtable}\n\\usepackage{booktabs}" :order 2) (cleveref :snippet "\\usepackage[capitalize]{cleveref}" :order 1) (underline :snippet "\\usepackage[normalem]{ulem}" :order 0.5) (float-wrap :snippet "\\usepackage{wrapfig}" :order 2) (rotate :snippet "\\usepackage{rotating}" :order 2) (caption :snippet org-latex-caption-preamble :order 2.1) (acronym :snippet "\\newcommand{\\acr}[1]{\\protect\\textls*[110]{\\scshape #1}}\n\\newcommand{\\acrs}{\\protect\\scalebox{.91}[.84]\\hspace{0.15ex}s}" :order 0.4) (italic-quotes :snippet "\\renewcommand{\\quote}{\\list{}{\\rightmargin\\leftmargin}\\item\\relax\\em}\n" :order 0.5) (par-sep :snippet "\\setlength{\\parskip}{\\baselineskip}\n\\setlength{\\parindent}{0pt}\n" :order 0.5) (.pifont :snippet "\\usepackage{pifont}") (checkbox :requires .pifont :order 3 :snippet (concat (unless (memq 'maths features) "\\usepackage{amssymb} % provides \\square") org-latex-checkbox-preamble)) (.fancy-box :requires .pifont :snippet org-latex-box-preamble :order 3.9) (box-warning :requires .fancy-box :snippet "\\defsimplebox{warning}{e66100}{\\ding{68}}{Warning}" :order 4) (box-info :requires .fancy-box :snippet "\\defsimplebox{info}{3584e4}{\\ding{68}}{Information}" :order 4) (box-success :requires .fancy-box :snippet "\\defsimplebox{success}{26a269}{\\ding{68}}{\\vspace{-\\baselineskip}}" :order 4) (box-error :requires .fancy-box :snippet "\\defsimplebox{error}{c01c28}{\\ding{68}}{Important}" :order 4)) "LaTeX features and details required to implement them. List where the car is the feature symbol, and the rest forms a plist with the following keys: - :snippet, which may be either - a string which should be included in the preamble - a symbol, the value of which is included in the preamble - a function, which is evaluated with the list of feature flags as its single argument. The result of which is included in the preamble - a list, which is passed to `eval', with a list of feature flags available as \"features\" - :requires, a feature or list of features that must be available - :when, a feature or list of features that when all available should cause this to be automatically enabled. - :prevents, a feature or list of features that should be masked - :order, for when ordering is important. Lower values appear first. The default is 0. Features that start with ! will be eagerly loaded, i.e. without being detected.")
Emacs Lisp
(defun org-latex-detect-features (&optional buffer info) "List features from `org-latex-conditional-features' detected in BUFFER." (let ((case-fold-search nil)) (with-current-buffer (or buffer (current-buffer)) (delete-dups (mapcan (lambda (construct-feature) (when (let ((out (pcase (car construct-feature) ((pred stringp) (car construct-feature)) ((pred functionp) (funcall (car construct-feature) info)) ((pred listp) (eval (car construct-feature))) ((pred symbolp) (symbol-value (car construct-feature))) (_ (user-error "org-latex-conditional-features key %s unable to be used" (car construct-feature)))))) (if (stringp out) (save-excursion (goto-char (point-min)) (re-search-forward out nil t)) out)) (if (listp (cdr construct-feature)) (cdr construct-feature) (list (cdr construct-feature))))) org-latex-conditional-features)))))
Emacs Lisp
(defun org-latex-expand-features (features) "For each feature in FEATURES process :requires, :when, and :prevents keywords and sort according to :order." (dolist (feature features) (unless (assoc feature org-latex-feature-implementations) (error "Feature %s not provided in org-latex-feature-implementations" feature))) (setq current features) (while current (when-let ((requirements (plist-get (cdr (assq (car current) org-latex-feature-implementations)) :requires))) (setcdr current (if (listp requirements) (append requirements (cdr current)) (cons requirements (cdr current))))) (setq current (cdr current))) (dolist (potential-feature (append features (delq nil (mapcar (lambda (feat) (when (plist-get (cdr feat) :eager) (car feat))) org-latex-feature-implementations)))) (when-let ((prerequisites (plist-get (cdr (assoc potential-feature org-latex-feature-implementations)) :when))) (setf features (if (if (listp prerequisites) (cl-every (lambda (preq) (memq preq features)) prerequisites) (memq prerequisites features)) (append (list potential-feature) features) (delq potential-feature features))))) (dolist (feature features) (when-let ((prevents (plist-get (cdr (assoc feature org-latex-feature-implementations)) :prevents))) (setf features (cl-set-difference features (if (listp prevents) prevents (list prevents)))))) (sort (delete-dups features) (lambda (feat1 feat2) (if (< (or (plist-get (cdr (assoc feat1 org-latex-feature-implementations)) :order) 1) (or (plist-get (cdr (assoc feat2 org-latex-feature-implementations)) :order) 1)) t nil))))
Emacs Lisp
(defun org-latex-generate-features-preamble (features) "Generate the LaTeX preamble content required to provide FEATURES. This is done according to `org-latex-feature-implementations'" (let ((expanded-features (org-latex-expand-features features))) (concat (format "\n%% features: %s\n" expanded-features) (mapconcat (lambda (feature) (when-let ((snippet (plist-get (cdr (assoc feature org-latex-feature-implementations)) :snippet))) (concat (pcase snippet ((pred stringp) snippet) ((pred functionp) (funcall snippet features)) ((pred listp) (eval `(let ((features ',features)) (,@snippet)))) ((pred symbolp) (symbol-value snippet)) (_ (user-error "org-latex-feature-implementations :snippet value %s unable to be used" snippet))) "\n"))) expanded-features "") "% end features\n")))
Emacs Lisp
(defvar info--tmp nil) (defadvice! org-latex-save-info (info &optional t_ s_) :before #'org-latex-make-preamble (setq info--tmp info)) (defadvice! org-splice-latex-header-and-generated-preamble-a (orig-fn tpl def-pkg pkg snippets-p &optional extra) "Dynamically insert preamble content based on `org-latex-conditional-preambles'." :around #'org-splice-latex-header (let ((header (funcall orig-fn tpl def-pkg pkg snippets-p extra))) (if snippets-p header (concat header (org-latex-generate-features-preamble (org-latex-detect-features nil info--tmp)) "\n"))))
- Embed Externally Linked Images
I don’t like to keep images downloaded to my laptop, it clutters up everything. Org has a handy feature where you can pass a link instead, and org will display it inline as usual.
HTML export handles this use case just fine, if the image isn’t named then it will display the image. However, latex doesn’t have support for this. What we do is instead of linking the image, we can have emacs download the linked image and export that!
Emacs Lisp
(defadvice! +org-latex-link (orig-fn link desc info) "Acts as `org-latex-link', but supports remote images." :around #'org-latex-link (setq o-link link o-desc desc o-info info) (if (and (member (plist-get (cadr link) :type) '("http" "https")) (member (file-name-extension (plist-get (cadr link) :path)) '("png" "jpg" "jpeg" "pdf" "svg"))) (org-latex-link--remote link desc info) (funcall orig-fn link desc info))) (defun org-latex-link--remote (link _desc info) (let* ((url (plist-get (cadr link) :raw-link)) (ext (file-name-extension url)) (target (format "%s%s.%s" (temporary-file-directory) (replace-regexp-in-string "[./]" "-" (file-name-sans-extension (substring (plist-get (cadr link) :path) 2))) ext))) (unless (file-exists-p target) (url-copy-file url target)) (setcdr link (--> (cadr link) (plist-put it :type "file") (plist-put it :path target) (plist-put it :raw-link (concat "file:" target)) (list it))) (concat "% fetched from " url "\n" (org-latex--inline-image link info))))
- LatexMK
Tectonic is the hot new thing, which also means I can get rid of my tex installation. Dependencies are nice and auto-installed, and I don’t need to bother with ascii stuff
On the other hand, it still refuses to work with previews and just sucks with emacs overall. Back to LatexMK for me
Emacs Lisp
(setq org-latex-pdf-process (list "latexmk -f -pdflatex='xelatex -shell-escape -interaction nonstopmode' -pdf -output-directory=%o %f")) (setq xdvsvgm '(xdvsvgm :programs ("xelatex" "dvisvgm") :description "xdv > svg" :message "you need to install the programs: xelatex and dvisvgm." :use-xcolor t :image-input-type "xdv" :image-output-type "svg" :image-size-adjust (1.7 . 1.5) :latex-compiler ("xelatex -no-pdf -interaction nonstopmode -output-directory %o %f") :image-converter ("dvisvgm %f -n -b min -c %S -o %O"))) (after! org (add-to-list 'org-preview-latex-process-alist xdvsvgm) (setq org-format-latex-options (plist-put org-format-latex-options :scale 1.4)) (setq org-preview-latex-default-process 'xdvsvgm))
Looks crisp!
\begin{align*} f(x) &= x^2\\ g(x) &= \frac{1}{x}\\ F(x) &= \int^a_b \frac{1}{3}x^3 \end{align*} - Classes
Now for some class setup
Emacs Lisp
(after! ox-latex (add-to-list 'org-latex-classes '("cb-doc" "\\documentclass{scrartcl}" ("\\section{%s}" . "\\section*{%s}") ("\\subsection{%s}" . "\\subsection*{%s}") ("\\subsubsection{%s}" . "\\subsubsection*{%s}") ("\\paragraph{%s}" . "\\paragraph*{%s}") ("\\subparagraph{%s}" . "\\subparagraph*{%s}"))))
And some saner defaults for them
Emacs Lisp
(after! ox-latex (setq org-latex-default-class "cb-doc" org-latex-tables-booktabs t org-latex-hyperref-template "\\colorlet{greenyblue}{blue!70!green} \\colorlet{blueygreen}{blue!40!green} \\providecolor{link}{named}{greenyblue} \\providecolor{cite}{named}{blueygreen} \\hypersetup{ pdfauthor={%a}, pdftitle={%t}, pdfkeywords={%k}, pdfsubject={%d}, pdfcreator={%c}, pdflang={%L}, breaklinks=true, colorlinks=true, linkcolor=, urlcolor=link, citecolor=cite\n} \\urlstyle{same} " org-latex-reference-command "\\cref{%s}"))
- Packages
Add some packages. I’m trying to keep it basic for now, Alegreya for non-monospace and SFMono for code
Emacs Lisp
(setq org-latex-default-packages-alist `(("AUTO" "inputenc" t ("pdflatex")) ("T1" "fontenc" t ("pdflatex")) ("" "fontspec" t) ("" "graphicx" t) ("" "grffile" t) ("" "longtable" nil) ("" "wrapfig" nil) ("" "rotating" nil) ("normalem" "ulem" t) ("" "amsmath" t) ("" "textcomp" t) ("" "amssymb" t) ("" "capt-of" nil) ("dvipsnames" "xcolor" nil) ("colorlinks=true, linkcolor=Blue, citecolor=BrickRed, urlcolor=PineGreen" "hyperref" nil) ("" "indentfirst" nil)))
- Pretty code blocks
Teco is the goto for this, so basically just ripping off him. Engrave faces ftw
Emacs Lisp
(use-package! engrave-faces-latex :after ox-latex :config (setq org-latex-listings 'engraved engrave-faces-preset-styles (engrave-faces-generate-preset)))
Emacs Lisp
(defadvice! org-latex-src-block-engraved (orig-fn src-block contents info) "Like `org-latex-src-block', but supporting an engraved backend" :around #'org-latex-src-block (if (eq 'engraved (plist-get info :latex-listings)) (org-latex-scr-block--engraved src-block contents info) (funcall orig-fn src-block contents info))) (defadvice! org-latex-inline-src-block-engraved (orig-fn inline-src-block contents info) "Like `org-latex-inline-src-block', but supporting an engraved backend" :around #'org-latex-inline-src-block (if (eq 'engraved (plist-get info :latex-listings)) (org-latex-inline-scr-block--engraved inline-src-block contents info) (funcall orig-fn src-block contents info))) (defvar-local org-export-has-code-p nil) (defadvice! org-export-expect-no-code (&rest _) :before #'org-export-as (setq org-export-has-code-p nil)) (defadvice! org-export-register-code (&rest _) :after #'org-latex-src-block-engraved :after #'org-latex-inline-src-block-engraved (setq org-export-has-code-p t)) (setq org-latex-engraved-code-preamble " \\usepackage{fvextra} \\fvset{ commandchars=\\\\\\{\\}, highlightcolor=white!95!black!80!blue, breaklines=true, breaksymbol=\\color{white!60!black}\\tiny\\ensuremath{\\hookrightarrow}} \\renewcommand\\theFancyVerbLine{\\footnotesize\\color{black!40!white}\\arabic{FancyVerbLine}} \\definecolor{codebackground}{HTML}{f7f7f7} \\definecolor{codeborder}{HTML}{f0f0f0} % TODO have code boxes keep line vertical alignment \\usepackage[breakable,xparse]{tcolorbox} \\DeclareTColorBox[]{Code}{o}% {colback=codebackground, colframe=codeborder, fontupper=\\footnotesize, colupper=EFD, IfNoValueTF={#1}% {boxsep=2pt, arc=2.5pt, outer arc=2.5pt, boxrule=0.5pt, left=2pt}% {boxsep=2.5pt, arc=0pt, outer arc=0pt, boxrule=0pt, leftrule=1.5pt, left=0.5pt}, right=2pt, top=1pt, bottom=0.5pt, breakable} ") (add-to-list 'org-latex-conditional-features '((and org-export-has-code-p "^[ \t]*#\\+begin_src\\|^[ \t]*#\\+BEGIN_SRC\\|src_[A-Za-z]") . engraved-code) t) (add-to-list 'org-latex-conditional-features '("^[ \t]*#\\+begin_example\\|^[ \t]*#\\+BEGIN_EXAMPLE" . engraved-code-setup) t) (add-to-list 'org-latex-feature-implementations '(engraved-code :requires engraved-code-setup :snippet (engrave-faces-latex-gen-preamble) :order 99) t) (add-to-list 'org-latex-feature-implementations '(engraved-code-setup :snippet org-latex-engraved-code-preamble :order 98) t) (defun org-latex-scr-block--engraved (src-block contents info) (let* ((lang (org-element-property :language src-block)) (attributes (org-export-read-attribute :attr_latex src-block)) (float (plist-get attributes :float)) (num-start (org-export-get-loc src-block info)) (retain-labels (org-element-property :retain-labels src-block)) (caption (org-element-property :caption src-block)) (caption-above-p (org-latex--caption-above-p src-block info)) (caption-str (org-latex--caption/label-string src-block info)) (placement (or (org-unbracket-string "[" "]" (plist-get attributes :placement)) (plist-get info :latex-default-figure-position))) (float-env (cond ((string= "multicolumn" float) (format "\\begin{listing*}[%s]\n%s%%s\n%s\\end{listing*}" placement (if caption-above-p caption-str "") (if caption-above-p "" caption-str))) (caption (format "\\begin{listing}[%s]\n%s%%s\n%s\\end{listing}" placement (if caption-above-p caption-str "") (if caption-above-p "" caption-str))) ((string= "t" float) (concat (format "\\begin{listing}[%s]\n" placement) "%s\n\\end{listing}")) (t "%s"))) (options (plist-get info :latex-minted-options)) (content-buffer (with-temp-buffer (insert (let* ((code-info (org-export-unravel-code src-block)) (max-width (apply 'max (mapcar 'length (org-split-string (car code-info) "\n"))))) (org-export-format-code (car code-info) (lambda (loc _num ref) (concat loc (when ref ;; Ensure references are flushed to the right, ;; separated with 6 spaces from the widest line ;; of code. (concat (make-string (+ (- max-width (length loc)) 6) ?\s) (format "(%s)" ref))))) nil (and retain-labels (cdr code-info))))) (funcall (org-src-get-lang-mode lang)) (engrave-faces-latex-buffer))) (content (with-current-buffer content-buffer (buffer-string))) (body (format "\\begin{Code}\n\\begin{Verbatim}[%s]\n%s\\end{Verbatim}\n\\end{Code}" ;; Options. (concat (org-latex--make-option-string (if (or (not num-start) (assoc "linenos" options)) options (append `(("linenos") ("firstnumber" ,(number-to-string (1+ num-start)))) options))) (let ((local-options (plist-get attributes :options))) (and local-options (concat "," local-options)))) content))) (kill-buffer content-buffer) ;; Return value. (format float-env body))) (defun org-latex-inline-scr-block--engraved (inline-src-block _contents info) (let ((options (org-latex--make-option-string (plist-get info :latex-minted-options))) code-buffer code) (setq code-buffer (with-temp-buffer (insert (org-element-property :value inline-src-block)) (funcall (org-src-get-lang-mode (org-element-property :language inline-src-block))) (engrave-faces-latex-buffer))) (setq code (with-current-buffer code-buffer (buffer-string))) (kill-buffer code-buffer) (format "\\Verb%s{%s}" (if (string= options "") "" (format "[%s]" options)) code))) (defadvice! org-latex-example-block-engraved (orig-fn example-block contents info) "Like `org-latex-example-block', but supporting an engraved backend" :around #'org-latex-example-block (let ((output-block (funcall orig-fn example-block contents info))) (if (eq 'engraved (plist-get info :latex-listings)) (format "\\begin{Code}[alt]\n%s\n\\end{Code}" output-block) output-block)))
- ox-chameleon
Nice little package to color stuff for us.
- Async
Run export processes in a background … process
- (sub|super)script characters
Annoying having to gate these, so let’s fix that
- Conditional features
- Calc
Embedded calc is a lovely feature which let’s us use calc to operate on LaTeX maths expressions. The standard keybinding is a bit janky however (C-x * e), so we’ll add a localleader-based alternative.
Emacs Lisp
(map! :map calc-mode-map :after calc :localleader :desc "Embedded calc (toggle)" "e" #'calc-embedded) (map! :map org-mode-map :after org :localleader :desc "Embedded calc (toggle)" "E" #'calc-embedded) (map! :map latex-mode-map :after latex :localleader :desc "Embedded calc (toggle)" "e" #'calc-embedded)
Unfortunately this operates without the (rather informative) calculator and trail buffers, but we can advice it that we would rather like those in a side panel.
Emacs Lisp
(defvar calc-embedded-trail-window nil) (defvar calc-embedded-calculator-window nil) (defadvice! calc-embedded-with-side-pannel (&rest _) :after #'calc-do-embedded (when calc-embedded-trail-window (ignore-errors (delete-window calc-embedded-trail-window)) (setq calc-embedded-trail-window nil)) (when calc-embedded-calculator-window (ignore-errors (delete-window calc-embedded-calculator-window)) (setq calc-embedded-calculator-window nil)) (when (and calc-embedded-info (> (* (window-width) (window-height)) 1200)) (let ((main-window (selected-window)) (vertical-p (> (window-width) 80))) (select-window (setq calc-embedded-trail-window (if vertical-p (split-window-horizontally (- (max 30 (/ (window-width) 3)))) (split-window-vertically (- (max 8 (/ (window-height) 4))))))) (switch-to-buffer "*Calc Trail*") (select-window (setq calc-embedded-calculator-window (if vertical-p (split-window-vertically -6) (split-window-horizontally (- (/ (window-width) 2)))))) (switch-to-buffer "*Calculator*") (select-window main-window))))
5.1.8. Mu4e
I’m trying out emails in emacs, should be nice. Related, check .mbsyncrc to setup your emails first
10 minutes is a reasonable update time
elisp
(set-email-account! "shaunsingh0207" '((mu4e-sent-folder . "/Sent Mail") (mu4e-drafts-folder . "/Drafts") (mu4e-trash-folder . "/Trash") (mu4e-refile-folder . "/All Mail") (smtpmail-smtp-user . "shaunsingh0207@gmail.com"))) ;; don't need to run cleanup after indexing for gmail (setq mu4e-index-cleanup nil mu4e-index-lazy-check t) (after! mu4e (setq mu4e-headers-fields '((:flags . 6) (:account-stripe . 2) (:from-or-to . 25) (:folder . 10) (:recipnum . 2) (:subject . 80) (:human-date . 8)) +mu4e-min-header-frame-width 142 mu4e-headers-date-format "%d/%m/%y" mu4e-headers-time-format "⧖ %H:%M" mu4e-headers-results-limit 1000 mu4e-index-cleanup t) (add-to-list 'mu4e-bookmarks '(:name "Yesterday's messages" :query "date:2d..1d" :key ?y) t) (defvar +mu4e-header--folder-colors nil) (appendq! mu4e-header-info-custom '((:folder . (:name "Folder" :shortname "Folder" :help "Lowest level folder" :function (lambda (msg) (+mu4e-colorize-str (replace-regexp-in-string "\\`.*/" "" (mu4e-message-field msg :maildir)) '+mu4e-header--folder-colors)))))))
We can also send messages using msmtp
Emacs Lisp
(after! mu4e (setq sendmail-program "msmtp" send-mail-function #'smtpmail-send-it message-sendmail-f-is-evil t message-sendmail-extra-arguments '("--read-envelope-from") message-send-mail-function #'message-send-mail-with-sendmail))
Notifications are quite nifty, especially if I’m as lazy as I am
5.1.9. Browsing
- Webkit
Eventually I want to use emacs for everything. Instead of using xwidgets, which requires a custom (non-cached) build of emacs. Emacs-webkit is a good alternative, but is quite buggy right now. Once its stable, I’ll fix this config
Emacs Lisp
;;(use-package org ;; :demand t) ;; (use-package webkit ;; :defer t ;; :commands webkit ;; :init ;; (setq webkit-search-prefix "https://google.com/search?q=" ;; webkit-history-file nil ;; webkit-cookie-file nil ;; browse-url-browser-function 'webkit-browse-url ;; webkit-browse-url-force-new t ;; webkit-download-action-alist '(("\\.pdf\\'" . webkit-download-open) ;; ("\\.png\\'" . webkit-download-save) ;; (".*" . webkit-download-default))) ;; (defun webkit--display-progress (progress) ;; (setq webkit--progress-formatted ;; (if (equal progress 100.0) ;; "" ;; (format "%s%.0f%% " (all-the-icons-faicon "spinner") progress))) ;; (force-mode-line-update)))
I also want to use evil bindings with this. It’s not upstreamed yet, so I’ll steal the ones from the repo
elisp
;; (use-package evil-collection-webkit ;; :defer t ;; :config ;; (evil-collection-xwidget-setup))
- IRC
I’m trying to move everything to emacs, and discord is the one electron app I need to ditch. With bitlbee and circe it should be possible
To make this easier, I
- Have everything (serverinfo and passwords) in an authinfo.gpg file
- Tell circe to use it
- Use org syntax for formatting
- Add emoji support
- Set it up with discord
irc-authinfo-readerEmacs Lisp
(defun auth-server-pass (server) (if-let ((secret (plist-get (car (auth-source-search :host server)) :secret))) (if (functionp secret) (funcall secret) secret) (error "Could not fetch password for host %s" server))) (defun register-irc-auths () (require 'circe) (require 'dash) (let ((accounts (-filter (lambda (a) (string= "irc" (plist-get a :for))) (auth-source-search :require '(:for) :max 10)))) (appendq! circe-network-options (mapcar (lambda (entry) (let* ((host (plist-get entry :host)) (label (or (plist-get entry :label) host)) (_ports (mapcar #'string-to-number (s-split "," (plist-get entry :port)))) (port (if (= 1 (length _ports)) (car _ports) _ports)) (user (plist-get entry :user)) (nick (or (plist-get entry :nick) user)) (channels (mapcar (lambda (c) (concat "#" c)) (s-split "," (plist-get entry :channels))))) `(,label :host ,host :port ,port :nick ,nick :sasl-username ,user :sasl-password auth-server-pass :channels ,channels))) accounts))))
We’ll just call
(register-irc-auths)
on a hook when we start Circe up.Now we’re ready to go, let’s actually wire-up Circe, with one or two configuration tweaks.
Emacs Lisp
(after! circe (setq-default circe-use-tls t) (setq circe-notifications-alert-icon "/usr/share/icons/breeze/actions/24/network-connect.svg" lui-logging-directory "~/.emacs.d/.local/etc/irc" lui-logging-file-format "{buffer}/%Y/%m-%d.txt" circe-format-self-say "{nick:+13s} ┃ {body}") (custom-set-faces! '(circe-my-message-face :weight unspecified)) (enable-lui-logging-globally) (enable-circe-display-images) <<org-emph-to-irc>> <<circe-emojis>> <<circe-emoji-alists>> (defun named-circe-prompt () (lui-set-prompt (concat (propertize (format "%13s > " (circe-nick)) 'face 'circe-prompt-face) ""))) (add-hook 'circe-chat-mode-hook #'named-circe-prompt) (appendq! all-the-icons-mode-icon-alist '((circe-channel-mode all-the-icons-material "message" :face all-the-icons-lblue) (circe-server-mode all-the-icons-material "chat_bubble_outline" :face all-the-icons-purple)))) <<irc-authinfo-reader>> (add-transient-hook! #'=irc (register-irc-auths))
Let’s do our bold, italic, and underline in org-syntax, using IRC control characters.
org-emph-to-ircEmacs Lisp
(defun lui-org-to-irc () "Examine a buffer with simple org-mode formatting, and converts the empasis: *bold*, /italic/, and _underline_ to IRC semi-standard escape codes. =code= is converted to inverse (highlighted) text." (goto-char (point-min)) (while (re-search-forward "\\_<\\(?1:[*/_=]\\)\\(?2:[^[:space:]]\\(?:.*?[^[:space:]]\\)?\\)\\1\\_>" nil t) (replace-match (concat (pcase (match-string 1) ("*" "") ("/" "") ("_" "") ("=" "")) (match-string 2) "") nil nil))) (add-hook 'lui-pre-input-hook #'lui-org-to-irc)
Let’s setup Circe to use some emojis
circe-emojisEmacs Lisp
(defun lui-ascii-to-emoji () (goto-char (point-min)) (while (re-search-forward "\\( \\)?::?\\([^[:space:]:]+\\):\\( \\)?" nil t) (replace-match (concat (match-string 1) (or (cdr (assoc (match-string 2) lui-emojis-alist)) (concat ":" (match-string 2) ":")) (match-string 3)) nil nil))) (defun lui-emoticon-to-emoji () (dolist (emoticon lui-emoticons-alist) (goto-char (point-min)) (while (re-search-forward (concat " " (car emoticon) "\\( \\)?") nil t) (replace-match (concat " " (cdr (assoc (cdr emoticon) lui-emojis-alist)) (match-string 1)))))) (define-minor-mode lui-emojify "Replace :emojis: and ;) emoticons with unicode emoji chars." :global t :init-value t (if lui-emojify (add-hook! lui-pre-input #'lui-ascii-to-emoji #'lui-emoticon-to-emoji) (remove-hook! lui-pre-input #'lui-ascii-to-emoji #'lui-emoticon-to-emoji)))
Now, some actual emojis to use.
circe-emoji-alistsEmacs Lisp
(defvar lui-emojis-alist '(("grinning" . "😀") ("smiley" . "😃") ("smile" . "😄") ("grin" . "😁") ("laughing" . "😆") ("sweat_smile" . "😅") ("joy" . "😂") ("rofl" . "🤣") ("relaxed" . "☺️") ("blush" . "😊") ("innocent" . "😇") ("slight_smile" . "🙂") ("upside_down" . "🙃") ("wink" . "😉") ("relieved" . "😌") ("heart_eyes" . "😍") ("yum" . "😋") ("stuck_out_tongue" . "😛") ("stuck_out_tongue_closed_eyes" . "😝") ("stuck_out_tongue_wink" . "😜") ("zanzy" . "🤪") ("raised_eyebrow" . "🤨") ("monocle" . "🧐") ("nerd" . "🤓") ("cool" . "😎") ("star_struck" . "🤩") ("party" . "🥳") ("smirk" . "😏") ("unamused" . "😒") ("disapointed" . "😞") ("pensive" . "😔") ("worried" . "😟") ("confused" . "😕") ("slight_frown" . "🙁") ("frown" . "☹️") ("persevere" . "😣") ("confounded" . "😖") ("tired" . "😫") ("weary" . "😩") ("pleading" . "🥺") ("tear" . "😢") ("cry" . "😢") ("sob" . "😭") ("triumph" . "😤") ("angry" . "😠") ("rage" . "😡") ("exploding_head" . "🤯") ("flushed" . "😳") ("hot" . "🥵") ("cold" . "🥶") ("scream" . "😱") ("fearful" . "😨") ("disapointed" . "😰") ("relieved" . "😥") ("sweat" . "😓") ("thinking" . "🤔") ("shush" . "🤫") ("liar" . "🤥") ("blank_face" . "😶") ("neutral" . "😐") ("expressionless" . "😑") ("grimace" . "😬") ("rolling_eyes" . "🙄") ("hushed" . "😯") ("frowning" . "😦") ("anguished" . "😧") ("wow" . "😮") ("astonished" . "😲") ("sleeping" . "😴") ("drooling" . "🤤") ("sleepy" . "😪") ("dizzy" . "😵") ("zipper_mouth" . "🤐") ("woozy" . "🥴") ("sick" . "🤢") ("vomiting" . "🤮") ("sneeze" . "🤧") ("mask" . "😷") ("bandaged_head" . "🤕") ("money_face" . "🤑") ("cowboy" . "🤠") ("imp" . "😈") ("ghost" . "👻") ("alien" . "👽") ("robot" . "🤖") ("clap" . "👏") ("thumpup" . "👍") ("+1" . "👍") ("thumbdown" . "👎") ("-1" . "👎") ("ok" . "👌") ("pinch" . "🤏") ("left" . "👈") ("right" . "👉") ("down" . "👇") ("wave" . "👋") ("pray" . "🙏") ("eyes" . "👀") ("brain" . "🧠") ("facepalm" . "🤦") ("tada" . "🎉") ("fire" . "🔥") ("flying_money" . "💸") ("lighbulb" . "💡") ("heart" . "❤️") ("sparkling_heart" . "💖") ("heartbreak" . "💔") ("100" . "💯"))) (defvar lui-emoticons-alist '((":)" . "slight_smile") (";)" . "wink") (":D" . "smile") ("=D" . "grin") ("xD" . "laughing") (";(" . "joy") (":P" . "stuck_out_tongue") (";D" . "stuck_out_tongue_wink") ("xP" . "stuck_out_tongue_closed_eyes") (":(" . "slight_frown") (";(" . "cry") (";'(" . "sob") (">:(" . "angry") (">>:(" . "rage") (":o" . "wow") (":O" . "astonished") (":/" . "confused") (":-/" . "thinking") (":|" . "neutral") (":-|" . "expressionless")))
5.2. Neovim
There are many neovim configurations that exist (i.e. NvChad, Lunar Vim, etc.). However, many of these configurations suffer from a host of problems:
Some configurations (like NvChad), have very abstracted and complex codebases. Others rely on having as much overall functionality as possible (like LunarVim). While none of this is bad, there are some problems that can arise from these choices:
Complex codebases lead to less freedom for end-user extensiblity and configuration, as there is more reliance on the maintainer of said code. Users may not use half of what is made avalible to them simply because they don’t need all of that functionality, so all of it may not be necessary. This config provides a solution to these problems by providing only the necessary code in order to make a functioning configuration. The end goal of this personal neovim config is to be used as a base config for users to extend and add upon, leading to a more unique editing experience.
The configuration was originally based off of commit 29f04fc of NvChad, but this config has evolved to be much more than that.
You can now find it seperately on github, here: https://github.com/shaunsingh/nyoom.nvim
5.2.1. Develop
When sharing the config, it makes it much easier to handle dependencies with nix. This ensures installing dependencies (including neovim-nightly), as well as adding the new neovim build to path
nix
{ description = ":rocket: Blazing Fast Neovim Configuration Written in lua :rocket::rocket::stars:"; inputs.flake-utils.url = "github:numtide/flake-utils"; inputs.neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay"; outputs = { self, nixpkgs, flake-utils, neovim-nightly-overlay }: flake-utils.lib.simpleFlake { inherit self nixpkgs; name = "default.nvim"; overlay = neovim-nightly-overlay.overlay; shell = ./shell.nix; systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ]; }; }
nix
{ pkgs ? import <nixpkgs> { overlays = [ (import (builtins.fetchTarball { url = "https://github.com/nix-community/neovim-nightly-overlay/archive/master.tar.gz"; })) ]; } }: with pkgs; mkShell { buildInputs = [ neovim-nightly luajit ripgrep nodejs ]; shellHook = '' alias nvim="nvim -u $(pwd)/init.lua" ''; }
5.2.2. Init
The init.lua first loads impatient.nvim if available (so we can cache the .lua files). It then disables builtin vim plugins, and loads the required modules for startup (packer_compiled.lua, mappings.lua, and options.lua)
Lua
--load impatient first local impatient, impatient = pcall(require, "impatient") if impatient then -- NOTE: currently broken, will fix soon --impatient.enable_profile() end --disable builtin plugins local disabled_built_ins = { "2html_plugin", "getscript", "getscriptPlugin", "gzip", "logipat", "netrw", "netrwPlugin", "netrwSettings", "netrwFileHandlers", "matchit", "tar", "tarPlugin", "rrhelper", "spellfile_plugin", "vimball", "vimballPlugin", "zip", "zipPlugin", } for _, plugin in pairs(disabled_built_ins) do vim.g["loaded_" .. plugin] = 1 end -- load options, mappings, and plugins local nyoom_modules = { "options", "mappings", "packer_compiled", } for i = 1, #nyoom_modules, 1 do pcall(require, nyoom_modules[i]) end
5.2.3. Packer
My packer configuration is broken into two files: packerInit and pluginList. packerInit downloads packer if it isn’t present, lazy loads it if it is, and configures packer. Notably:
- Put the packer_compiled file under /nvim/lua instead of /nvim/plugin so it can be chached by impatient.nvim
- Use packer in a floating window instead of a split, and remove the borders
- Increaes clone_timeout, just in case I’m on a more finicky network
pluginList contains the list of plugins, as well lazy loads and defines their configuration files.
Lua
vim.cmd "packadd packer.nvim" local present, packer = pcall(require, "packer") --clone packer if its missing if not present then local packer_path = vim.fn.stdpath "data" .. "/site/pack/packer/opt/packer.nvim" print "Cloning packer.." -- remove the dir before cloning vim.fn.delete(packer_path, "rf") vim.fn.system { "git", "clone", "https://github.com/wbthomason/packer.nvim", "--depth", "20", packer_path, } vim.cmd "packadd packer.nvim" present, packer = pcall(require, "packer") if present then print "Packer cloned successfully." else error("Couldn't clone packer !\nPacker path: " .. packer_path) end end -- packer settings return packer.init { -- tell packer to put packer_compiled under the /lua folder so its cached by impatient compile_path = vim.fn.stdpath "config" .. "/lua/packer_compiled.lua", display = { open_fn = function() return require("packer.util").float { border = "rounded" } end, prompt_border = "rounded", }, git = { clone_timeout = 600, -- Timeout, in seconds, for git clones }, }
Lua
local present, packer = pcall(require, "packerInit") if present then packer = require "packer" else return false end local use = packer.use return packer.startup(function() -- Have packer manage itself use { "wbthomason/packer.nvim", event = "VimEnter", } -- Startup optimizations use { "nathom/filetype.nvim", } use { "lewis6991/impatient.nvim", } use { "tweekmonster/startuptime.vim", cmd = "StartupTime", } use { "max397574/better-escape.nvim", event = "InsertEnter", config = function() require("better_escape").setup { mapping = { "jk", "jj" }, clear_empty_lines = true, keys = "<Esc>", } end, } use { "Clutch-Squad-10669/nord.nvim", config = function() require("nord").set() end, -- "~/.config/nvim/lua/ext/doom.nvim", -- config = function() -- vim.cmd[[colorscheme doom]] -- end, } use { "folke/which-key.nvim", keys = "<space>", config = function() require("which-key").setup() end, } use { "kyazdani42/nvim-web-devicons", } use { "~/.config/nvim/lua/ext/statusline.nvim", config = function() require "plugins.statusline" end, } use { "akinsho/bufferline.nvim", config = function() require "plugins.bufferline" end, } use { "lukas-reineke/indent-blankline.nvim", config = function() require("plugins.others").blankline() end, } use { "norcalli/nvim-colorizer.lua", cmd = "ColorizerToggle", config = function() require("plugins.others").colorizer() end, } use { "nvim-treesitter/nvim-treesitter", config = function() require "plugins.treesitter" end, } use { "nvim-treesitter/playground", cmd = "TSPlayground", } use { "p00f/nvim-ts-rainbow", after = "nvim-treesitter", } use { "lewis6991/gitsigns.nvim", } use { "kyazdani42/nvim-tree.lua", cmd = { "NvimTreeToggle", "NvimTreeFocus" }, config = function() require "plugins.nvimtree" end, } -- LSP (and copilot use { "github/copilot.vim", event = "InsertEnter", } use { "dccsillag/magma-nvim", cmd = { "MagmaInit" }, run = ":UpdateRemotePlugins", } use { "neovim/nvim-lspconfig", config = function() require "plugins.lspconfig" end, } use { "williamboman/nvim-lsp-installer", } use { "ray-x/lsp_signature.nvim", after = "nvim-lspconfig", config = function() require("plugins.others").signature() end, } use { "rafamadriz/friendly-snippets", event = "InsertEnter", } use { "numToStr/Comment.nvim", after = "friendly-snippets", config = function() require("plugins.others").comment() end, } use { "hrsh7th/nvim-cmp", after = "friendly-snippets", config = function() require "plugins.cmp" end, } use { "L3MON4D3/LuaSnip", wants = "friendly-snippets", after = "nvim-cmp", config = function() require("plugins.others").luasnip() end, } use { "saadparwaiz1/cmp_luasnip", after = "LuaSnip", } use { "hrsh7th/cmp-nvim-lua", after = "nvim-cmp", } use { "hrsh7th/cmp-nvim-lsp", after = "nvim-cmp", } use { "lukas-reineke/cmp-rg", after = "nvim-cmp", } use { "ray-x/cmp-treesitter", after = "nvim-cmp", } use { "hrsh7th/cmp-path", after = "nvim-cmp", } use { "nvim-telescope/telescope.nvim", cmd = "Telescope", requires = { { "nvim-telescope/telescope-fzf-native.nvim", "nvim-lua/plenary.nvim", run = "make", }, }, config = function() require "plugins.telescope" end, } use { "VonHeikemen/fine-cmdline.nvim", requires = { "MunifTanjim/nui.nvim", }, config = function() require("plugins.others").fineCmdline() end, } use { "VonHeikemen/searchbox.nvim", requires = { "MunifTanjim/nui.nvim", }, config = function() require("plugins.others").searchbox() end, } use { "rcarriga/nvim-notify", config = function() vim.notify = require "notify" require("notify").setup { stages = "slide", timeout = 2500, minimum_width = 50, icons = { ERROR = "", WARN = "", INFO = "", DEBUG = "", TRACE = "✎", }, } end, } use { "Pocco81/TrueZen.nvim", cmd = { "TZAtaraxis", "TZMinimalist", "TZFocus", }, config = function() require "plugins.zenmode" end, } use { "folke/twilight.nvim", cmd = { "Twilight", "TwilightEnable", }, config = function() require("twilight").setup {} end, } use { "phaazon/hop.nvim", cmd = { "HopWord", "HopLine", "HopChar1", "HopChar2", "HopPattern", }, as = "hop", config = function() require("hop").setup() end, } use { "sindrets/diffview.nvim", after = "neogit", } use { "TimUntersberger/neogit", cmd = { "Neogit", "Neogit commit", }, config = function() require "plugins.neogit" end, } use { "nvim-neorg/neorg", branch = "unstable", setup = vim.cmd "autocmd BufRead,BufNewFile *.norg setlocal filetype=norg", after = { "nvim-treesitter" }, -- you may also specify telescope ft = "norg", config = function() require "plugins.neorg" end, } use { "nvim-orgmode/orgmode", ft = "org", setup = vim.cmd "autocmd BufRead,BufNewFile *.org setlocal filetype=org", after = { "nvim-treesitter" }, config = function() require("orgmode").setup {} end, } use { "nvim-neorg/neorg-telescope", ft = "norg", } end)
5.2.4. Settings
As I said earlier, there are 3 required modules for startup (packer_compiled.lua, mappings.lua, and options.lua). Of that, packer_compiled.lua is generated using :PackerCompile, so we will focus on the other two.
- mappings.lua contains all of my mappings. All of the mappings are the same as the defaults for Doom Emacs (with a few exceptions). To list all of the keybinds, run SPC h b b in doom (or SPC h b f for major-mode specific binds). The file also contains some commands, which allow for the lazy loading of packer.
- options.lua contains all the basic options I want set before loading a buffer. Additionally, I want to disable the tilde fringe and filetype.vim (replaced with filetype.nvim).
Lua
-- helper function for clean mappings local function map(mode, lhs, rhs, opts) local options = { noremap = true, silent = true } if opts then options = vim.tbl_extend("force", options, opts) end vim.api.nvim_set_keymap(mode, lhs, rhs, options) end vim.g.mapleader = " " --leader map("n", ";", ":") --semicolon to enter command mode map("n", "<leader>ww", "<cmd>HopWord<CR>") --easymotion/hop map("n", "<leader>l", "<cmd>HopLine<CR>") map("n", "<leader>fr", "<cmd>Telescope oldfiles<CR>") --fuzzy map("n", "<leader>.", "<cmd>Telescope find_files<CR>") map("n", "<leader>f", "<cmd>Telescope current_buffer_fuzzy_find<CR>") map("n", "<leader>:", "<cmd>Telescope commands<CR>") map("n", "<leader>bb", "<cmd>Telescope buffers<CR>") map("n", "<leader>tz", "<cmd>TZAtaraxis<CR>") --ataraxis map("n", "<leader>op", "<cmd>NvimTreeToggle<CR>") --nvimtree map("n", "<leader>tw", "<cmd>set wrap!<CR>") --nvimtree map("n", "<c-k>", "<cmd>wincmd k<CR>") --ctrlhjkl to navigate splits map("n", "<c-j>", "<cmd>wincmd j<CR>") map("n", "<c-h>", "<cmd>wincmd h<CR>") map("n", "<c-l>", "<cmd>wincmd l<CR>") -- since we lazy load packer.nvim, we need to load it when we run packer-related commands vim.cmd "silent! command PackerCompile lua require 'pluginList' require('packer').compile()" vim.cmd "silent! command PackerInstall lua require 'pluginList' require('packer').install()" vim.cmd "silent! command PackerStatus lua require 'pluginList' require('packer').status()" vim.cmd "silent! command PackerSync lua require 'pluginList' require('packer').sync()" vim.cmd "silent! command PackerUpdate lua require 'pluginList' require('packer').update()"
Lua
-- Do not source the default filetype.vim vim.g.did_load_filetypes = 1 vim.g.shell = "/bin/bash" --fish has speed issues with nvim-tree vim.g.neovide_cursor_vfx_mode = "pixiedust" -- neovide trail --vim.opt.fillchars = { eob = " " } -- disable tilde fringe vim.opt.undofile = true -- enable persistent undo vim.opt.swapfile = false -- disable swap vim.opt.cursorline = true -- enable cursorline vim.opt.mouse = "a" -- enable mouse vim.opt.signcolumn = "yes" -- enable signcolumn vim.opt.updatetime = 250 vim.opt.clipboard = "unnamedplus" -- enable universal clipboard vim.opt.scrolloff = 3 -- leave 3 lines up/down while scrolling vim.opt.tabstop = 4 -- tabs should be 4 "space" wide vim.opt.shiftwidth = 4 -- tabs should be 4 "space" wide vim.opt.expandtab = true -- tabs should be 4 "space" wide vim.opt.linebreak = true -- clean linebreaks vim.opt.number = false -- disable numbers vim.opt.numberwidth = 2 -- two wide number column vim.opt.guifont = "Liga SFMono Nerd Font:h14" -- set guifont for neovide vim.opt.shortmess:append "casI" -- disable intro vim.opt.whichwrap:append "<>hl" -- clean aligned wraps vim.opt.guicursor:append "i:blinkwait700-blinkon400-blinkoff250" --Remap for dealing with word wrap vim.api.nvim_set_keymap("n", "k", "v:count == 0 ? 'gk' : 'k'", { noremap = true, expr = true, silent = true }) vim.api.nvim_set_keymap("n", "j", "v:count == 0 ? 'gj' : 'j'", { noremap = true, expr = true, silent = true }) -- Highlight on yank vim.cmd [[ augroup YankHighlight autocmd! autocmd TextYankPost * silent! lua vim.highlight.on_yank() augroup end ]]
5.2.5. Plugin Configuration
- Bufferline
Lua
local present, bufferline = pcall(require, "bufferline") if not present then return end local colors = { bg = "NONE", black = "#242730", black2 = "#2a2e38", white = "#bbc2cf", fg = "#bbc2cf", yellow = "#FCCE7B", cyan = "#4db5bd", darkblue = "#51afef", green = "#7bc275", orange = "#e69055", purple = "#C57BDB", magenta = "#C57BDB", gray = "#62686E", blue = "#51afef", red = "#ff665c", } bufferline.setup { options = { offsets = { { filetype = "NvimTree", text = "", padding = 1 } }, buffer_close_icon = "", modified_icon = "", close_icon = "λ", show_close_icon = true, left_trunc_marker = "", right_trunc_marker = "", max_name_length = 14, max_prefix_length = 13, tab_size = 20, show_tab_indicators = true, enforce_regular_tabs = false, view = "multiwindow", show_buffer_close_icons = true, separator_style = "thin", always_show_bufferline = false, diagnostics = false, -- "or nvim_lsp" custom_filter = function(buf_number) -- Func to filter out our managed/persistent split terms local present_type, type = pcall(function() return vim.api.nvim_buf_get_var(buf_number, "term_type") end) if present_type then if type == "vert" then return false elseif type == "hori" then return false else return true end else return true end end, }, highlights = { background = { guifg = colors.fg, guibg = colors.black2, }, -- buffers buffer_selected = { guifg = colors.white, guibg = colors.black, gui = "bold", }, buffer_visible = { guifg = colors.gray, guibg = colors.black2, }, -- for diagnostics = "nvim_lsp" error = { guifg = colors.gray, guibg = colors.black2, }, error_diagnostic = { guifg = colors.gray, guibg = colors.black2, }, -- close buttons close_button = { guifg = colors.gray, guibg = colors.black2, }, close_button_visible = { guifg = colors.gray, guibg = colors.black2, }, close_button_selected = { guifg = colors.red, guibg = colors.black, }, fill = { guifg = colors.fg, guibg = colors.black2, }, indicator_selected = { guifg = colors.black, guibg = colors.black, }, -- modified modified = { guifg = colors.red, guibg = colors.black2, }, modified_visible = { guifg = colors.red, guibg = colors.black2, }, modified_selected = { guifg = colors.green, guibg = colors.black, }, -- separators separator = { guifg = colors.black2, guibg = colors.black2, }, separator_visible = { guifg = colors.black2, guibg = colors.black2, }, separator_selected = { guifg = colors.black2, guibg = colors.black2, }, -- tabs tab = { guifg = colors.gray, guibg = colors.black2, }, tab_selected = { guifg = colors.black2, guibg = colors.darkblue, }, tab_close = { guifg = colors.red, guibg = colors.black, }, }, }
- Nvim-cmp
Lua
local present, cmp = pcall(require, "cmp") if not present then return end vim.opt.completeopt = "menuone,noselect" -- nvim-cmp setup cmp.setup { snippet = { expand = function(args) require("luasnip").lsp_expand(args.body) end, }, formatting = { format = function(entry, vim_item) vim_item.menu = ({ rg = "rg", nvim_lsp = "LSP", nvim_lua = "Lua", Path = "Path", luasnip = "LuaSnip", neorg = "Neorg", orgmode = "Org", treesitter = "ts", })[entry.source.name] vim_item.kind = ({ Text = "", Method = "", Function = "", Constructor = "", Field = "ﰠ", Variable = "", Class = "ﴯ", Interface = "", Module = "", Property = "ﰠ", Unit = "塞", Value = "", Enum = "", Keyword = "", Snippet = "", Color = "", File = "", Reference = "", Folder = "", EnumMember = "", Constant = "", Struct = "פּ", Event = "", Operator = "", TypeParameter = "", })[vim_item.kind] return vim_item end, }, mapping = { ["<C-p>"] = cmp.mapping.select_prev_item(), ["<C-n>"] = cmp.mapping.select_next_item(), ["<C-d>"] = cmp.mapping.scroll_docs(-4), ["<C-f>"] = cmp.mapping.scroll_docs(4), ["<C-Space>"] = cmp.mapping.complete(), ["<C-e>"] = cmp.mapping.close(), ["<CR>"] = cmp.mapping.confirm { behavior = cmp.ConfirmBehavior.Replace, select = true, }, ["<Tab>"] = function(fallback) if cmp.visible() then cmp.select_next_item() elseif require("luasnip").expand_or_jumpable() then vim.fn.feedkeys(vim.api.nvim_replace_termcodes("<Plug>luasnip-expand-or-jump", true, true, true), "") else fallback() end end, ["<S-Tab>"] = function(fallback) if cmp.visible() then cmp.select_prev_item() elseif require("luasnip").jumpable(-1) then vim.fn.feedkeys(vim.api.nvim_replace_termcodes("<Plug>luasnip-jump-prev", true, true, true), "") else fallback() end end, }, sources = { { name = "nvim_lsp" }, { name = "luasnip" }, { name = "rg" }, { name = "nvim_lua" }, { name = "neorg" }, { name = "treesitter" }, { name = "orgmode" }, }, }
- Gitsigns
Lua
local present, gitsigns = pcall(require, "gitsigns") if not present then return end gitsigns.setup { signs = { add = { hl = "GitSignsAdd", text = "┃", numhl = "GitSignsAddNr", linehl = "GitSignsAddLn" }, change = { hl = "GitSignsChange", text = "┃", numhl = "GitSignsChangeNr", linehl = "GitSignsChangeLn" }, delete = { hl = "GitSignsDelete", text = "▁", numhl = "GitSignsDeleteNr", linehl = "GitSignsDeleteLn" }, topdelete = { hl = "GitSignsDelete", text = "▔", numhl = "GitSignsDeleteNr", linehl = "GitSignsDeleteLn" }, changedelete = { hl = "GitSignsChange", text = "~", numhl = "GitSignsChangeNr", linehl = "GitSignsChangeLn" }, }, signcolumn = true, -- Toggle with `:Gitsigns toggle_signs` numhl = false, -- Toggle with `:Gitsigns toggle_numhl` linehl = false, -- Toggle with `:Gitsigns toggle_linehl` keymaps = { -- Default keymap options noremap = true, ["n ]c"] = { expr = true, "&diff ? ']c' : '<cmd>lua require\"gitsigns.actions\".next_hunk()<CR>'" }, ["n [c"] = { expr = true, "&diff ? '[c' : '<cmd>lua require\"gitsigns.actions\".prev_hunk()<CR>'" }, ["n <leader>hs"] = '<cmd>lua require"gitsigns".stage_hunk()<CR>', ["v <leader>hs"] = '<cmd>lua require"gitsigns".stage_hunk({vim.fn.line("."), vim.fn.line("v")})<CR>', ["n <leader>hu"] = '<cmd>lua require"gitsigns".undo_stage_hunk()<CR>', ["n <leader>hr"] = '<cmd>lua require"gitsigns".reset_hunk()<CR>', ["v <leader>hr"] = '<cmd>lua require"gitsigns".reset_hunk({vim.fn.line("."), vim.fn.line("v")})<CR>', ["n <leader>hR"] = '<cmd>lua require"gitsigns".reset_buffer()<CR>', ["n <leader>hp"] = '<cmd>lua require"gitsigns".preview_hunk()<CR>', ["n <leader>hb"] = '<cmd>lua require"gitsigns".blame_line(true)<CR>', ["n <leader>hS"] = '<cmd>lua require"gitsigns".stage_buffer()<CR>', ["n <leader>hU"] = '<cmd>lua require"gitsigns".reset_buffer_index()<CR>', -- Text objects ["o ih"] = ':<C-U>lua require"gitsigns.actions".select_hunk()<CR>', ["x ih"] = ':<C-U>lua require"gitsigns.actions".select_hunk()<CR>', }, update_debounce = 200, }
- Lspconfig
Lua
local present1, lspconfig = pcall(require, "lspconfig") local present2, lsp_installer = pcall(require, "nvim-lsp-installer") if not (present1 or present2) then return end local capabilities = vim.lsp.protocol.make_client_capabilities() capabilities.textDocument.completion.completionItem.documentationFormat = { "markdown", "plaintext" } capabilities.textDocument.completion.completionItem.snippetSupport = true capabilities.textDocument.completion.completionItem.preselectSupport = true capabilities.textDocument.completion.completionItem.insertReplaceSupport = true capabilities.textDocument.completion.completionItem.labelDetailsSupport = true capabilities.textDocument.completion.completionItem.deprecatedSupport = true capabilities.textDocument.completion.completionItem.commitCharactersSupport = true capabilities.textDocument.completion.completionItem.tagSupport = { valueSet = { 1 } } capabilities.textDocument.completion.completionItem.resolveSupport = { properties = { "documentation", "detail", "additionalTextEdits", }, } -- lazy load servers lsp_installer.on_server_ready(function(server) local opts = {} server:setup(opts) vim.cmd [[ do User LspAttachBuffers ]] end) -- replace the default lsp diagnostic symbols local function lspSymbol(name, icon) vim.fn.sign_define("LspDiagnosticsSign" .. name, { text = icon, numhl = "LspDiagnosticsDefaul" .. name }) end lspSymbol("Error", "") lspSymbol("Information", "") lspSymbol("Hint", "") lspSymbol("Warning", "") -- add smol icon before diagnostics vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { virtual_text = { prefix = "", spacing = 0, }, signs = true, underline = true, update_in_insert = false, -- update diagnostics insert mode }) -- rounded borders vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(vim.lsp.handlers.hover, { border = "rounded" }) vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(vim.lsp.handlers.signature_help, { border = "rounded" })
- Neogit
Lua
local present, neogit = pcall(require, "neogit") if not present then return end neogit.setup { disable_signs = false, disable_context_highlighting = false, disable_commit_confirmation = false, -- customize displayed signs signs = { -- { CLOSED, OPENED } section = { "", "" }, item = { "", "" }, hunk = { "", "" }, }, integrations = { diffview = true, }, }
- Neorg
Lua
local present, neorg = pcall(require, "neorg") if not present then return end neorg.setup { -- Tell Neorg what modules to load load = { ["core.defaults"] = {}, -- Load all the default modules ["core.norg.concealer"] = {}, -- Allows for use of icons ["core.norg.dirman"] = { -- Manage your directories with Neorg config = { workspaces = { my_workspace = "~/org/neorg", }, }, }, ["core.norg.completion"] = { config = { engine = "nvim-cmp", -- We current support nvim-compe and nvim-cmp only }, }, ["core.keybinds"] = { -- Configure core.keybinds config = { default_keybinds = true, -- Generate the default keybinds neorg_leader = "<Leader>o", -- This is the default if unspecified }, }, ["core.integrations.telescope"] = {}, -- Enable the telescope module }, }
- Nvimtree
Lua
local present, nvimtree = pcall(require, "nvim-tree") if not present then return end vim.o.termguicolors = true vim.cmd [[highlight NvimTreeNormal guifg=#D8DEE9 guibg=#2a2e39]] vim.g.nvim_tree_add_trailing = 0 -- append a trailing slash to folder names vim.g.nvim_tree_highlight_opened_files = 0 vim.g.nvim_tree_indent_markers = 1 vim.g.nvim_tree_ignore = { ".git", "node_modules", ".cache" } vim.g.nvim_tree_quit_on_open = 0 -- closes tree when file's opened vim.g.nvim_tree_root_folder_modifier = table.concat { ":t:gs?$?/..", string.rep(" ", 1000), "?:gs?^??" } -- vim.g.nvim_tree_show_icons = { folders = 1, files = 1, } vim.g.nvim_tree_icons = { default = "", symlink = "", git = { deleted = "", ignored = "◌", renamed = "➜", staged = "✓", unmered = "", unstaged = "✗", untracked = "★", }, folder = { arrow_open = "", arrow_closed = "", default = "", empty = "", empty_open = "", open = "", symlink = "", symlink_open = "", }, } nvimtree.setup { filters = { dotfiles = false, }, disable_netrw = true, hijack_netrw = true, ignore_ft_on_setup = { "dashboard" }, auto_close = false, open_on_tab = false, hijack_cursor = true, update_cwd = true, update_focused_file = { enable = true, update_cwd = true, }, view = { allow_resize = true, side = "left", width = 30, }, }
- Others
Lua
local M = {} M.colorizer = function() local present, colorizer = pcall(require, "colorizer") if present then colorizer.setup({ "*" }, { RGB = true, -- #RGB hex codes RRGGBB = true, -- #RRGGBB hex codes names = true, -- "Name" codes like Blue RRGGBBAA = true, -- #RRGGBBAA hex codes rgb_fn = true, -- CSS rgb() and rgba() functions hsl_fn = true, -- CSS hsl() and hsla() functions css = true, -- Enable all CSS features: rgb_fn, hsl_fn, names, RGB, RRGGBB css_fn = true, -- Enable all CSS *functions*: rgb_fn, hsl_fn mode = "foreground", -- Set the display mode. }) vim.cmd "ColorizerReloadAllBuffers" end end M.fineCmdline = function() local present, fineCmdline = pcall(require, "fine-cmdline") if present then --remap ex-commands to floating windows vim.api.nvim_set_keymap("n", ":", ':lua require("fine-cmdline").open()<CR>', { noremap = true }) fineCmdline.setup { cmdline = { smart_history = true, }, popup = { border = { style = "rounded", highlight = "TelescopeResultsBorder", }, }, } end end M.searchbox = function() local searchbox = pcall(require, "searchbox") if searchbox then vim.api.nvim_set_keymap( "n", "/", ':lua require("searchbox").match_all({clear_matches = true})<CR>', { noremap = true } ) vim.api.nvim_set_keymap( "n", "<leader>/", ':lua require("searchbox").replace({confirm = "menu"})<CR>', { noremap = true } ) vim.api.nvim_set_keymap( "v", "<leader>/", '<Esc><cmd>lua require("searchbox").replace({exact = true, visual_mode = true, confirm = "menu"})<CR>', { noremap = true } ) end end M.blankline = function() require("indent_blankline").setup { show_current_context = true, context_patterns = { "class", "return", "function", "method", "^if", "^while", "jsx_element", "^for", "^object", "^table", "block", "arguments", "if_statement", "else_clause", "jsx_element", "jsx_self_closing_element", "try_statement", "catch_clause", "import_statement", "operation_type", }, filetype_exclude = { "help", "terminal", "dashboard", "packer", "lspinfo", "TelescopePrompt", "TelescopeResults", }, buftype_exclude = { "terminal" }, show_trailing_blankline_indent = false, show_first_indent_level = false, } end M.luasnip = function() local present, luasnip = pcall(require, "luasnip") if not present then return end luasnip.config.set_config { history = true, updateevents = "TextChanged,TextChangedI", } require("luasnip/loaders/from_vscode").load() end M.signature = function() local present, lspsignature = pcall(require, "lsp_signature") if present then lspsignature.setup { bind = true, doc_lines = 2, floating_window = true, fix_pos = true, hint_enable = true, hint_prefix = " ", hint_scheme = "String", hi_parameter = "Search", max_height = 22, max_width = 120, -- max_width of signature floating_window, line will be wrapped if exceed max_width handler_opts = { border = "single", -- double, single, shadow, none }, zindex = 200, -- by default it will be on top of all floating windows, set to 50 send it to bottom padding = "", -- character to pad on left and right of signature can be ' ', or '|' etc } end end M.comment = function() local present, comment = pcall(require, "Commment") if present then comment.setup { padding = true, } end end M.orgmode = function() local present, orgmode = pcall(require, "orgmode") if present then orgmode.setup({ "*" }, { org_highlight_latex_and_related = "entities", org_agenda_files = "~/org/*", org_default_notes_file = "~/org/notes.org", org_hide_leading_stars = true, org_hide_emphasis_markers = true, mappings = { global = { org_agenda = "<Leader>oa", org_capture = "<Leader>oc", }, agenda = { org_agenda_later = "f", org_agenda_earlier = "b", org_agenda_goto_today = ".", org_agenda_day_view = "vd", org_agenda_week_view = "vw", org_agenda_month_view = "vm", org_agenda_year_view = "vy", org_agenda_quit = "q", org_agenda_switch_to = "<CR>", org_agenda_goto = { "<TAB>" }, org_agenda_goto_date = "J", org_agenda_redo = "r", org_agenda_todo = "t", org_agenda_show_help = "?", }, capture = { org_capture_finalize = "<C-c>", org_capture_refile = "<Leader>or", org_capture_kill = "<Leader>ok", org_capture_show_help = "?", }, org = { org_increase_date = "<C-a>", org_decrease_date = "<C-x>", org_toggle_checkbox = "<C-Space>", org_open_at_point = "<Leader>oo", org_cycle = "<TAB>", org_global_cycle = "<S-TAB>", org_archive_subtree = "<Leader>o$", org_set_tags_command = "<Leader>ot", org_toggle_archive_tag = "<Leader>oA", org_do_promote = "<<", org_do_demote = ">>", org_promote_subtree = "<s", org_demote_subtree = ">s", org_meta_return = "<Leader><CR>", -- Add headling, item or row org_insert_heading_respect_content = "<Leader>oih", -- Add new headling after current heading block with same level org_insert_todo_heading = "<Leader>oiT", -- Add new todo headling right after current heading with same level org_insert_todo_heading_respect_content = "<Leader>oit", -- Add new todo headling after current heading block on same level org_move_subtree_up = "<Leader>oK", org_move_subtree_down = "<Leader>oJ", org_export = "<Leader>oe", org_next_visible_heading = "}", org_previous_visible_heading = "{", org_forward_heading_same_level = "]]", org_backward_heading_same_level = "[[", }, }, }) end end return M
- Statusline
Lua
local present, statusline = pcall(require, "statusline") if not present then return end statusline.lsp_diagnostics = true
- Telescope
Lua
local present, telescope = pcall(require, "telescope") if not present then return end telescope.setup { defaults = { vimgrep_arguments = { "rg", "--color=never", "--no-heading", "--with-filename", "--line-number", "--column", "--smart-case", }, prompt_prefix = " λ ", selection_caret = " > ", entry_prefix = " ", initial_mode = "insert", selection_strategy = "reset", sorting_strategy = "ascending", layout_strategy = "horizontal", layout_config = { horizontal = { prompt_position = "top", preview_width = 0.55, results_width = 0.8, }, vertical = { mirror = false, }, width = 0.87, height = 0.80, preview_cutoff = 120, }, file_sorter = require("telescope.sorters").get_fuzzy_file, file_ignore_patterns = {}, generic_sorter = require("telescope.sorters").get_generic_fuzzy_sorter, path_display = { "absolute" }, winblend = 0, border = {}, borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, color_devicons = true, use_less = true, set_env = { ["COLORTERM"] = "truecolor" }, -- default = nil, file_previewer = require("telescope.previewers").vim_buffer_cat.new, grep_previewer = require("telescope.previewers").vim_buffer_vimgrep.new, qflist_previewer = require("telescope.previewers").vim_buffer_qflist.new, -- Developer configurations: Not meant for general override buffer_previewer_maker = require("telescope.previewers").buffer_previewer_maker, }, extensions = { fzf = { fuzzy = true, -- false will only do exact matching override_generic_sorter = false, -- override the generic sorter override_file_sorter = true, -- override the file sorter case_mode = "smart_case", -- or "ignore_case" or "respect_case" -- the default case_mode is "smart_case" }, }, } local extensions = { "themes", "terms", "fzf" } local packer_repos = [["extensions", "telescope-fzf-native.nvim"]] pcall(function() for _, ext in ipairs(extensions) do telescope.load_extension(ext) end end)
- Treesitter
Lua
local present, ts_config = pcall(require, "nvim-treesitter.configs") if not present then return end local parser_configs = require("nvim-treesitter.parsers").get_parser_configs() parser_configs.norg = { -- on macOS: https://github.com/nvim-neorg/neorg/issues/74#issuecomment-906627223 install_info = { url = "https://github.com/nvim-neorg/tree-sitter-norg", files = { "src/parser.c", "src/scanner.cc" }, branch = "main", }, } parser_configs.org = { install_info = { url = "https://github.com/milisims/tree-sitter-org", revision = "main", files = { "src/parser.c", "src/scanner.cc" }, }, filetype = "org", } ts_config.setup { ensure_installed = { "lua", "nix" }, indent = { enable = true }, highlight = { enable = true, use_languagetree = true, additional_vim_regex_highlighting = { "org" }, }, rainbow = { enable = true, extended_mode = true, -- Also highlight non-bracket delimiters like html tags, boolean or table: lang -> boolean max_file_lines = nil, -- Do not enable for files with more than n lines, int }, playground = { enable = true, updatetime = 25, -- Debounced time for highlighting nodes in the playground from source code persist_queries = false, -- Whether the query persists across vim sessions }, incremental_selection = { enable = true, keymaps = { init_selection = "<CR>", scope_incremental = "<CR>", node_incremental = "<TAB>", node_decremental = "<S-TAB>", }, }, textobjects = { select = { enable = true, lookahead = true, -- Automatically jump forward to textobj, similar to targets.vim keymaps = { -- You can use the capture groups defined in textobjects.scm ["af"] = "@function.outer", ["if"] = "@function.inner", ["ac"] = "@class.outer", ["ic"] = "@class.inner", }, }, move = { enable = true, set_jumps = true, -- whether to set jumps in the jumplist goto_next_start = { ["]m"] = "@function.outer", ["]]"] = "@class.outer", }, goto_next_end = { ["]M"] = "@function.outer", ["]["] = "@class.outer", }, goto_previous_start = { ["[m"] = "@function.outer", ["[["] = "@class.outer", }, goto_previous_end = { ["[M"] = "@function.outer", ["[]"] = "@class.outer", }, }, }, }
- Zenmode
Lua
local present, true_zen = pcall(require, "true-zen") if not present then return end true_zen.setup { ui = { bottom = { cmdheight = 1, laststatus = 0, ruler = true, showmode = true, showcmd = false, }, top = { showtabline = 0, }, left = { number = false, relativenumber = false, signcolumn = "no", }, }, modes = { ataraxis = { left_padding = 32, right_padding = 32, top_padding = 1, bottom_padding = 1, ideal_writing_area_width = { 0 }, auto_padding = false, keep_default_fold_fillchars = false, bg_configuration = true, }, focus = { margin_of_error = 5, focus_method = "experimental", }, }, integrations = { galaxyline = true, nvim_bufferline = true, twilight = true, }, }
5.2.6. External
These are plugin I’m currently developing, but don’t want ready for public release yet. As such, they reside in the nyoom.nvim/lua/ext directory.
- doom.nvim
This is an experimental port of doom-vibrant to neovim, just me playing around with the new highlight API
Lua
-- Colorscheme name: doom.nvim -- Description: Port of hlissner's doom-vibrant theme for neovim -- Author: https://github.com/shaunsingh local doom = { --16 colors doom0_gui = "#242730", doom1_gui = "#2a2e38", doom2_gui = "#484854", doom3_gui = "#62686E", doom4_gui = "#bbc2cf", doom5_gui = "#5D656B", doom6_gui = "#bbc2cf", doom7_gui = "#4db5bd", doom8_gui = "#5cEfFF", doom9_gui = "#51afef", doom10_gui = "#C57BDB", doom11_gui = "#ff665c", doom12_gui = "#e69055", doom13_gui = "#FCCE7B", doom14_gui = "#7bc275", doom15_gui = "#C57BDB", none = "NONE", } -- Syntax highlight groups local syntax = { Type = { fg = doom.doom9_gui }, -- int, long, char, etc. StorageClass = { fg = doom.doom9_gui }, -- static, register, volatile, etc. Structure = { fg = doom.doom9_gui }, -- struct, union, enum, etc. Constant = { fg = doom.doom4_gui }, -- any constant Character = { fg = doom.doom14_gui }, -- any character constant: 'c', '\n' Number = { fg = doom.doom15_gui }, -- a number constant: 5 Boolean = { fg = doom.doom9_gui }, -- a boolean constant: TRUE, false Float = { fg = doom.doom15_gui }, -- a floating point constant: 2.3e10 Statement = { fg = doom.doom9_gui }, -- any statement Label = { fg = doom.doom9_gui }, -- case, default, etc. Operator = { fg = doom.doom9_gui }, -- sizeof", "+", "*", etc. Exception = { fg = doom.doom9_gui }, -- try, catch, throw PreProc = { fg = doom.doom9_gui }, -- generic Preprocessor Include = { fg = doom.doom9_gui }, -- preprocessor #include Define = { fg = doom.doom9_gui }, -- preprocessor #define Macro = { fg = doom.doom9_gui }, -- same as Define Typedef = { fg = doom.doom9_gui }, -- A typedef PreCondit = { fg = doom.doom13_gui }, -- preprocessor #if, #else, #endif, etc. Special = { fg = doom.doom4_gui }, -- any special symbol SpecialChar = { fg = doom.doom13_gui }, -- special character in a constant Tag = { fg = doom.doom4_gui }, -- you can use CTRL-] on this Delimiter = { fg = doom.doom6_gui }, -- character that needs attention like , or . SpecialComment = { fg = doom.doom8_gui }, -- special things inside a comment Debug = { fg = doom.doom11_gui }, -- debugging statements Underlined = { fg = doom.doom14_gui, bg = doom.none, style = "underline" }, -- text that stands out, HTML links Ignore = { fg = doom.doom1_gui }, -- left blank, hidden Error = { fg = doom.doom11_gui, bg = doom.none, style = "bold,underline" }, -- any erroneous construct Todo = { fg = doom.doom13_gui, bg = doom.none, style = "bold" }, Conceal = { fg = doom.none, bg = doom.doom0_gui }, htmlLink = { fg = doom.doom14_gui, style = "underline" }, htmlH1 = { fg = doom.doom8_gui, style = "bold" }, htmlH2 = { fg = doom.doom11_gui, style = "bold" }, htmlH3 = { fg = doom.doom14_gui, style = "bold" }, htmlH4 = { fg = doom.doom15_gui, style = "bold" }, htmlH5 = { fg = doom.doom9_gui, style = "bold" }, markdownH1 = { fg = doom.doom8_gui, style = "bold" }, markdownH2 = { fg = doom.doom11_gui, style = "bold" }, markdownH3 = { fg = doom.doom14_gui, style = "bold" }, markdownH1Delimiter = { fg = doom.doom8_gui }, markdownH2Delimiter = { fg = doom.doom11_gui }, markdownH3Delimiter = { fg = doom.doom14_gui }, Comment = { fg = doom.doom3_gui }, -- normal comments Conditional = { fg = doom.doom9_gui }, -- normal if, then, else, endif, switch, etc. Keyword = { fg = doom.doom9_gui }, -- normal for, do, while, etc. Repeat = { fg = doom.doom9_gui }, -- normal any other keyword Function = { fg = doom.doom8_gui }, -- normal function names Identifier = { fg = doom.doom9_gui }, -- any variable name String = { fg = doom.doom14_gui }, -- any string } -- Editor highlight groups local editor = { NormalFloat = { fg = doom.doom4_gui, bg = doom.doom0_gui }, -- normal text and background color ColorColumn = { fg = doom.none, bg = doom.doom1_gui }, -- used for the columns set with 'colorcolumn' Conceal = { fg = doom.doom1_gui }, -- placeholder characters substituted for concealed text (see 'conceallevel') Cursor = { fg = doom.doom4_gui, bg = doom.none, style = "reverse" }, -- the character under the cursor CursorIM = { fg = doom.doom5_gui, bg = doom.none, style = "reverse" }, -- like Cursor, but used when in IME mode Directory = { fg = doom.doom7_gui, bg = doom.none }, -- directory names (and other special names in listings) DiffAdd = { fg = doom.doom14_gui, bg = doom.none, style = "reverse" }, -- diff mode: Added line DiffChange = { fg = doom.doom13_gui, bg = doom.none, style = "reverse" }, -- diff mode: Changed line DiffDelete = { fg = doom.doom11_gui, bg = doom.none, style = "reverse" }, -- diff mode: Deleted line DiffText = { fg = doom.doom15_gui, bg = doom.none, style = "reverse" }, -- diff mode: Changed text within a changed line EndOfBuffer = { fg = doom.doom1_gui }, ErrorMsg = { fg = doom.none }, Folded = { fg = doom.doom_3_gui_bright, bg = doom.none, style = "italic" }, FoldColumn = { fg = doom.doom7_gui }, IncSearch = { fg = doom.doom6_gui, bg = doom.doom10_gui }, LineNr = { fg = doom.doom3_gui }, CursorLineNr = { fg = doom.doom4_gui }, MatchParen = { fg = doom.doom15_gui, bg = doom.none, style = "bold" }, ModeMsg = { fg = doom.doom4_gui }, MoreMsg = { fg = doom.doom4_gui }, NonText = { fg = doom.doom1_gui }, Pmenu = { fg = doom.doom4_gui, bg = doom.doom2_gui }, PmenuSel = { fg = doom.doom4_gui, bg = doom.doom10_gui }, PmenuSbar = { fg = doom.doom4_gui, bg = doom.doom2_gui }, PmenuThumb = { fg = doom.doom4_gui, bg = doom.doom4_gui }, Question = { fg = doom.doom14_gui }, QuickFixLine = { fg = doom.doom4_gui, bg = doom.doom6_gui, style = "reverse" }, qfLineNr = { fg = doom.doom4_gui, bg = doom.doom6_gui, style = "reverse" }, Search = { fg = doom.doom1_gui, bg = doom.doom6_gui, style = "reverse" }, SpecialKey = { fg = doom.doom9_gui }, SpellBad = { fg = doom.doom11_gui, bg = doom.none, style = "italic,undercurl" }, SpellCap = { fg = doom.doom7_gui, bg = doom.none, style = "italic,undercurl" }, SpellLocal = { fg = doom.doom8_gui, bg = doom.none, style = "italic,undercurl" }, SpellRare = { fg = doom.doom9_gui, bg = doom.none, style = "italic,undercurl" }, StatusLine = { fg = doom.doom4_gui, bg = doom.doom2_gui }, StatusLineNC = { fg = doom.doom4_gui, bg = doom.doom1_gui }, StatusLineTerm = { fg = doom.doom4_gui, bg = doom.doom2_gui }, StatusLineTermNC = { fg = doom.doom4_gui, bg = doom.doom1_gui }, TabLineFill = { fg = doom.doom4_gui }, TablineSel = { fg = doom.doom8_gui, bg = doom.doom3_gui }, Tabline = { fg = doom.doom4_gui }, Title = { fg = doom.doom14_gui, bg = doom.none, style = "bold" }, Visual = { fg = doom.none, bg = doom.doom1_gui }, VisualNOS = { fg = doom.none, bg = doom.doom1_gui }, WarningMsg = { fg = doom.doom15_gui }, WildMenu = { fg = doom.doom12_gui, bg = doom.none, style = "bold" }, CursorColumn = { fg = doom.none, bg = doom.doom1_gui }, CursorLine = { fg = doom.none, bg = doom.doom1_gui }, ToolbarLine = { fg = doom.doom4_gui, bg = doom.doom1_gui }, ToolbarButton = { fg = doom.doom4_gui, bg = doom.none, style = "bold" }, NormalMode = { fg = doom.doom4_gui, bg = doom.none, style = "reverse" }, InsertMode = { fg = doom.doom14_gui, bg = doom.none, style = "reverse" }, ReplacelMode = { fg = doom.doom11_gui, bg = doom.none, style = "reverse" }, VisualMode = { fg = doom.doom9_gui, bg = doom.none, style = "reverse" }, CommandMode = { fg = doom.doom4_gui, bg = doom.none, style = "reverse" }, Warnings = { fg = doom.doom15_gui }, healthError = { fg = doom.doom11_gui }, healthSuccess = { fg = doom.doom14_gui }, healthWarning = { fg = doom.doom15_gui }, -- dashboard DashboardShortCut = { fg = doom.doom7_gui }, DashboardHeader = { fg = doom.doom9_gui }, DashboardCenter = { fg = doom.doom8_gui }, DashboardFooter = { fg = doom.doom14_gui, style = "italic" }, -- BufferLine BufferLineIndicatorSelected = { fg = doom.doom0_gui }, BufferLineFill = { bg = doom.doom0_gui }, Normal = { fg = doom.doom4_gui, bg = doom.doom0_gui }, SignColumn = { fg = doom.doom4_gui, bg = doom.doom0_gui }, VertSplit = { fg = doom.doom0_gui }, } -- TreeSitter highlight groups local treesitter = { TSCharacter = { fg = doom.doom14_gui }, -- For characters. TSConstructor = { fg = doom.doom9_gui }, -- For constructor calls and definitions: `= { }` in Lua, and Java constructors. TSConstant = { fg = doom.doom13_gui }, -- For constants TSFloat = { fg = doom.doom15_gui }, -- For floats TSNumber = { fg = doom.doom15_gui }, -- For all number TSString = { fg = doom.doom14_gui }, -- For strings. TSAttribute = { fg = doom.doom15_gui }, -- (unstable) TODO: docs TSBoolean = { fg = doom.doom9_gui }, -- For booleans. TSConstBuiltin = { fg = doom.doom7_gui }, -- For constant that are built in the language: `nil` in Lua. TSConstMacro = { fg = doom.doom7_gui }, -- For constants that are defined by macros: `NULL` in C. TSError = { fg = doom.doom11_gui }, -- For syntax/parser errors. TSException = { fg = doom.doom15_gui }, -- For exception related keywords. TSField = { fg = doom.doom4_gui }, -- For fields. TSFuncMacro = { fg = doom.doom7_gui }, -- For macro defined fuctions (calls and definitions): each `macro_rules` in Rust. TSInclude = { fg = doom.doom9_gui }, -- For includes: `#include` in C, `use` or `extern crate` in Rust, or `require` in Lua. TSLabel = { fg = doom.doom15_gui }, -- For labels: `label:` in C and `:label:` in Lua. TSNamespace = { fg = doom.doom4_gui }, -- For identifiers referring to modules and namespaces. TSOperator = { fg = doom.doom9_gui }, -- For any operator: `+`, but also `->` and `*` in C. TSParameter = { fg = doom.doom10_gui }, -- For parameters of a function. TSParameterReference = { fg = doom.doom10_gui }, -- For references to parameters of a function. TSProperty = { fg = doom.doom10_gui }, -- Same as `TSField`. TSPunctDelimiter = { fg = doom.doom8_gui }, -- For delimiters ie: `.` TSPunctBracket = { fg = doom.doom8_gui }, -- For brackets and parens. TSPunctSpecial = { fg = doom.doom8_gui }, -- For special punctutation that does not fall in the catagories before. TSStringRegex = { fg = doom.doom7_gui }, -- For regexes. TSStringEscape = { fg = doom.doom15_gui }, -- For escape characters within a string. TSSymbol = { fg = doom.doom15_gui }, -- For identifiers referring to symbols or atoms. TSType = { fg = doom.doom9_gui }, -- For types. TSTypeBuiltin = { fg = doom.doom9_gui }, -- For builtin types. TSTag = { fg = doom.doom4_gui }, -- Tags like html tag names. TSTagDelimiter = { fg = doom.doom15_gui }, -- Tag delimiter like `<` `>` `/` TSText = { fg = doom.doom4_gui }, -- For strings considedoom11_gui text in a markup language. TSTextReference = { fg = doom.doom15_gui }, -- FIXME TSEmphasis = { fg = doom.doom10_gui }, -- For text to be represented with emphasis. TSUnderline = { fg = doom.doom4_gui, bg = doom.none, style = "underline" }, -- For text to be represented with an underline. TSTitle = { fg = doom.doom10_gui, bg = doom.none, style = "bold" }, -- Text that is part of a title. TSLiteral = { fg = doom.doom4_gui }, -- Literal text. TSURI = { fg = doom.doom14_gui }, -- Any URI like a link or email. TSAnnotation = { fg = doom.doom11_gui }, -- For C++/Dart attributes, annotations that can be attached to the code to denote some kind of meta information. TSComment = { fg = doom.doom3_gui }, TSConditional = { fg = doom.doom9_gui }, -- For keywords related to conditionnals. TSKeyword = { fg = doom.doom9_gui }, -- For keywords that don't fall in previous categories. TSRepeat = { fg = doom.doom9_gui }, -- For keywords related to loops. TSKeywordFunction = { fg = doom.doom8_gui }, TSFunction = { fg = doom.doom8_gui }, -- For fuction (calls and definitions). TSMethod = { fg = doom.doom7_gui }, -- For method calls and definitions. TSFuncBuiltin = { fg = doom.doom8_gui }, TSVariable = { fg = doom.doom4_gui }, -- Any variable name that does not have another highlight. TSVariableBuiltin = { fg = doom.doom4_gui }, } -- Lsp highlight groups local lsp = { DiagnosticDefaultError = { fg = doom.doom11_gui }, -- used for "Error" diagnostic virtual text DiagnosticSignError = { fg = doom.doom11_gui }, -- used for "Error" diagnostic signs in sign column DiagnosticFloatingError = { fg = doom.doom11_gui }, -- used for "Error" diagnostic messages in the diagnostics float DiagnosticVirtualTextError = { fg = doom.doom11_gui }, -- Virtual text "Error" DiagnosticUnderlineError = { fg = doom.doom11_gui, style = "undercurl" }, -- used to underline "Error" diagnostics. DiagnosticDefaultWarn = { fg = doom.doom15_gui }, -- used for "Warn" diagnostic signs in sign column DiagnosticSignWarn = { fg = doom.doom15_gui }, -- used for "Warn" diagnostic signs in sign column DiagnosticFloatingWarn = { fg = doom.doom15_gui }, -- used for "Warn" diagnostic messages in the diagnostics float DiagnosticVirtualTextWarn = { fg = doom.doom15_gui }, -- Virtual text "Warn" DiagnosticUnderlineWarn = { fg = doom.doom15_gui, style = "undercurl" }, -- used to underline "Warn" diagnostics. DiagnosticDefaultInfo = { fg = doom.doom10_gui }, -- used for "Info" diagnostic virtual text DiagnosticSignInfo = { fg = doom.doom10_gui }, -- used for "Info" diagnostic signs in sign column DiagnosticFloatingInfo = { fg = doom.doom10_gui }, -- used for "Info" diagnostic messages in the diagnostics float DiagnosticVirtualTextInfo = { fg = doom.doom10_gui }, -- Virtual text "Info" DiagnosticUnderlineInfo = { fg = doom.doom10_gui, style = "undercurl" }, -- used to underline "Info" diagnostics. DiagnosticDefaultHint = { fg = doom.doom9_gui }, -- used for "Hint" diagnostic virtual text DiagnosticSignHint = { fg = doom.doom9_gui }, -- used for "Hint" diagnostic signs in sign column DiagnosticFloatingHint = { fg = doom.doom9_gui }, -- used for "Hint" diagnostic messages in the diagnostics float DiagnosticVirtualTextHint = { fg = doom.doom9_gui }, -- Virtual text "Hint" DiagnosticUnderlineHint = { fg = doom.doom10_gui, style = "undercurl" }, -- used to underline "Hint" diagnostics. LspReferenceText = { fg = doom.doom4_gui, bg = doom.doom1_gui }, -- used for highlighting "text" references LspReferenceRead = { fg = doom.doom4_gui, bg = doom.doom1_gui }, -- used for highlighting "read" references LspReferenceWrite = { fg = doom.doom4_gui, bg = doom.doom1_gui }, -- used for highlighting "write" references } -- Plugins highlight groups local plugins = { -- LspTrouble LspTroubleText = { fg = doom.doom4_gui }, LspTroubleCount = { fg = doom.doom9_gui, bg = doom.doom10_gui }, LspTroubleNormal = { fg = doom.doom4_gui, bg = doom.doom0_gui }, -- Diff diffAdded = { fg = doom.doom14_gui }, diffRemoved = { fg = doom.doom11_gui }, diffChanged = { fg = doom.doom15_gui }, diffOldFile = { fg = doom.yelow }, diffNewFile = { fg = doom.doom12_gui }, diffFile = { fg = doom.doom7_gui }, diffLine = { fg = doom.doom3_gui }, diffIndexLine = { fg = doom.doom9_gui }, -- Neogit NeogitBranch = { fg = doom.doom10_gui }, NeogitRemote = { fg = doom.doom9_gui }, NeogitHunkHeader = { fg = doom.doom8_gui }, NeogitHunkHeaderHighlight = { fg = doom.doom8_gui, bg = doom.doom1_gui }, NeogitDiffContextHighlight = { bg = doom.doom1_gui }, NeogitDiffDeleteHighlight = { fg = doom.doom11_gui, style = "reverse" }, NeogitDiffAddHighlight = { fg = doom.doom14_gui, style = "reverse" }, -- GitGutter GitGutterAdd = { fg = doom.doom14_gui }, -- diff mode: Added line |diff.txt| GitGutterChange = { fg = doom.doom15_gui }, -- diff mode: Changed line |diff.txt| GitGutterDelete = { fg = doom.doom11_gui }, -- diff mode: Deleted line |diff.txt| -- GitSigns GitSignsAdd = { fg = doom.doom14_gui }, -- diff mode: Added line |diff.txt| GitSignsAddNr = { fg = doom.doom14_gui }, -- diff mode: Added line |diff.txt| GitSignsAddLn = { fg = doom.doom14_gui }, -- diff mode: Added line |diff.txt| GitSignsChange = { fg = doom.doom15_gui }, -- diff mode: Changed line |diff.txt| GitSignsChangeNr = { fg = doom.doom15_gui }, -- diff mode: Changed line |diff.txt| GitSignsChangeLn = { fg = doom.doom15_gui }, -- diff mode: Changed line |diff.txt| GitSignsDelete = { fg = doom.doom11_gui }, -- diff mode: Deleted line |diff.txt| GitSignsDeleteNr = { fg = doom.doom11_gui }, -- diff mode: Deleted line |diff.txt| GitSignsDeleteLn = { fg = doom.doom11_gui }, -- diff mode: Deleted line |diff.txt| -- Telescope TelescopePromptBorder = { fg = doom.doom8_gui }, TelescopeResultsBorder = { fg = doom.doom9_gui }, TelescopePreviewBorder = { fg = doom.doom14_gui }, TelescopeSelectionCaret = { fg = doom.doom9_gui }, TelescopeSelection = { fg = doom.doom9_gui }, TelescopeMatching = { fg = doom.doom8_gui }, -- NvimTree NvimTreeNormal = { fg = doom.doom4_gui, bg = doom.doom0_gui }, NvimTreeRootFolder = { fg = doom.doom7_gui, style = "bold" }, NvimTreeGitDirty = { fg = doom.doom15_gui }, NvimTreeGitNew = { fg = doom.doom14_gui }, NvimTreeImageFile = { fg = doom.doom15_gui }, NvimTreeExecFile = { fg = doom.doom14_gui }, NvimTreeSpecialFile = { fg = doom.doom9_gui, style = "underline" }, NvimTreeFolderName = { fg = doom.doom10_gui }, NvimTreeEmptyFolderName = { fg = doom.doom1_gui }, NvimTreeFolderIcon = { fg = doom.doom4_gui }, NvimTreeIndentMarker = { fg = doom.doom1_gui }, LspDiagnosticsError = { fg = doom.doom11_gui }, LspDiagnosticsWarning = { fg = doom.doom15_gui }, LspDiagnosticsInformation = { fg = doom.doom10_gui }, LspDiagnosticsHint = { fg = doom.doom9_gui }, -- WhichKey WhichKey = { fg = doom.doom4_gui, style = "bold" }, WhichKeyGroup = { fg = doom.doom4_gui }, WhichKeyDesc = { fg = doom.doom7_gui, style = "italic" }, WhichKeySeperator = { fg = doom.doom4_gui }, WhichKeyFloating = { bg = doom.doom0_gui }, WhichKeyFloat = { bg = doom.doom0_gui }, -- Sneak Sneak = { fg = doom.doom0_gui, bg = doom.doom4_gui }, SneakScope = { bg = doom.doom1_gui }, -- Cmp CmpItemKind = { fg = doom.doom15_gui }, CmpItemAbbrMatch = { fg = doom.doom9_gui, style = "bold" }, CmpItemAbbrMatchFuzzy = { fg = doom.doom14_gui, style = "bold" }, CmpItemAbbr = { fg = doom.doom13_gui }, CmpItemMenu = { fg = doom.doom14_gui }, -- Indent Blankline IndentBlanklineChar = { fg = doom.doom3_gui }, IndentBlanklineContextChar = { fg = doom.doom10_gui }, -- Illuminate illuminatedWord = { bg = doom.doom3_gui }, illuminatedCurWord = { bg = doom.doom3_gui }, -- nvim-dap DapBreakpoint = { fg = doom.doom14_gui }, DapStopped = { fg = doom.doom15_gui }, -- Hop HopNextKey = { fg = doom.doom4_gui, style = "bold" }, HopNextKey1 = { fg = doom.doom8_gui, style = "bold" }, HopNextKey2 = { fg = doom.doom4_gui }, HopUnmatched = { fg = doom.doom3_gui }, -- Fern FernBranchText = { fg = doom.doom3_gui }, -- nvim-ts-rainbow rainbowcol1 = { fg = doom.doom15_gui }, rainbowcol2 = { fg = doom.doom13_gui }, rainbowcol3 = { fg = doom.doom11_gui }, rainbowcol4 = { fg = doom.doom7_gui }, rainbowcol5 = { fg = doom.doom8_gui }, rainbowcol6 = { fg = doom.doom15_gui }, rainbowcol7 = { fg = doom.doom13_gui }, } local set_namespace = vim.api.nvim__set_hl_ns or vim.api.nvim_set_hl_ns local namespace = vim.api.nvim_create_namespace "doom" local function highlight(statement) for name, setting in pairs(statement) do vim.api.nvim_set_hl(namespace, name, setting) end end local M = {} M.setup = function() vim.cmd "highlight clear" vim.o.background = "dark" vim.o.termguicolors = true vim.g.colors_name = "doom" if vim.fn.exists "syntax_on" then vim.cmd "syntax reset" end highlight(syntax) highlight(editor) highlight(treesitter) highlight(plugins) highlight(lsp) set_namespace(namespace) end return M
- Statusline.nvim
Just a little statusline to mess around with
Lua
local statusline = require "modules.statusline" local M = {} M.lsp_diagnostics = true -- Enable Nvim native LSP as default -- TODO: Clean up this mess function M.activeLine() if M.lsp_diagnostics == true then vim.wo.statusline = "%!v:lua.require'modules.statusline'.wants_lsp()" else vim.wo.statusline = "%!v:lua.require'modules.statusline'.activeLine()" end end function M.simpleLine() vim.wo.statusline = statusline.simpleLine() end function M.inActiveLine() vim.wo.statusline = statusline.inActiveLine() end return M
- Modules
This is the fun part!
Lua
local M = {} local _config = {} local function get_defaults() return { options = { lsp_client = "native", }, } end local function merge(defaults, user_config) if user_config and type(user_config) == "table" then user_config = vim.tbl_deep_extend("force", defaults, user_config) end return user_config end function M.set(user_config) user_config = user_config or {} local defaults = get_defaults() _config = merge(defaults, user_config) return _config end
Lua
local modes = require "tables._modes" local git_branch = require "sections._git_branch" local lsp = require "sections._lsp" local signify = require "sections._signify" local bufmod = require "sections._bufmodified" local bufname = require "sections._bufname" local buficon = require "sections._buficon" local editable = require "sections._bufeditable" local filesize = require "sections._filesize" local M = {} -- Separators local left_separator = "" local right_separator = "" -- Blank Between Components local space = " " -- Different colors for mode local purple = "#C57BDB" local blue = "#51afef" local yellow = "#FCCE7B" local green = "#7bc275" local red = "#ff665c" -- fg and bg local white_fg = "#bbc2cf" local black_fg = "#242730" --Statusline colour local statusline_bg = "None" --> Set to none, use native bg local statusline_fg = white_fg -- local statusline_font = 'regular' vim.api.nvim_command("hi Status_Line guibg=" .. statusline_bg .. " guifg=" .. statusline_fg) --LSP Function Highlight Color vim.api.nvim_command("hi Statusline_LSP_Func guibg=" .. statusline_bg .. " guifg=" .. green) -- INACTIVE BUFFER Colours local InactiveLine_bg = "#2a2e38" local InactiveLine_fg = white_fg vim.api.nvim_command("hi InActive guibg=" .. InactiveLine_bg .. " guifg=" .. InactiveLine_fg) -- Redraw different colors for different mode local set_mode_colours = function(mode) if mode == "n" then vim.api.nvim_command("hi Mode guibg=" .. blue .. " guifg=" .. black_fg .. " gui=bold") vim.api.nvim_command("hi ModeSeparator guifg=" .. blue) end if mode == "i" then vim.api.nvim_command("hi Mode guibg=" .. green .. " guifg=" .. black_fg .. " gui=bold") vim.api.nvim_command("hi ModeSeparator guifg=" .. green) end if mode == "v" or mode == "V" or mode == "^V" then vim.api.nvim_command("hi Mode guibg=" .. purple .. " guifg=" .. black_fg .. " gui=bold") vim.api.nvim_command("hi ModeSeparator guifg=" .. purple) end if mode == "c" then vim.api.nvim_command("hi Mode guibg=" .. yellow .. " guifg=" .. black_fg .. " gui=bold") vim.api.nvim_command("hi ModeSeparator guifg=" .. yellow) end if mode == "t" then vim.api.nvim_command("hi Mode guibg=" .. red .. " guifg=" .. black_fg .. " gui=bold") vim.api.nvim_command("hi ModeSeparator guifg=" .. red) end end function M.activeLine() local statusline = "" -- Component: Mode local mode = vim.api.nvim_get_mode()["mode"] set_mode_colours(mode) statusline = statusline .. "%#ModeSeparator#" .. space statusline = statusline .. "%#ModeSeparator#" .. left_separator .. "%#Mode# " .. modes.current_mode[mode] .. " %#ModeSeparator#" .. right_separator .. space -- Component: Filetype and icons statusline = statusline .. "%#Status_Line#" .. bufname.get_buffer_name() statusline = statusline .. buficon.get_file_icon() if diag_lsp then statusline = statusline .. lsp.diagnostics() end statusline = statusline .. signify.signify() statusline = statusline .. git_branch.branch() statusline = statusline .. lsp.lsp_progress() statusline = statusline .. "%=" -- Component: LSP CURRENT FUCTION --> Requires LSP statusline = statusline .. "%#Statusline_LSP_Func# " .. lsp.current_function() statusline = statusline .. "%#Statusline_LSP_Func# " .. lsp.lightbulb() -- Component: Modified, Read-Only, Filesize, Row/Col statusline = statusline .. "%#Status_Line#" .. bufmod.is_buffer_modified() statusline = statusline .. editable.editable() .. filesize.get_file_size() .. [[ʟ %l/%L c %c]] .. space vim.api.nvim_command "set noruler" return statusline end function M.wants_lsp() diag_lsp = true return M.activeLine(diag_lsp) end -- statusline for simple buffers such as NvimTree where you don't need mode indicators etc function M.simpleLine() local statusline = "" return statusline .. "%#Status_Line#" .. bufname.get_buffer_name() .. "" end -- INACTIVE FUNCTION DISPLAY function M.inActiveLine() local statusline = "" return statusline .. bufname.get_buffer_name() .. buficon.get_file_icon() end return M
- Sections
All the sections of the statusline, lsp, git, etc,
Lua
local M = {} -- local vim = vim function M.editable() if vim.bo.filetype == "help" then return "" end if vim.bo.readonly == true then return " " end return "" end return M
Lua
local M = {} local api = vim.api local icons = require "tables._icons" local space = " " function M.get_file_icon() local file_name = api.nvim_buf_get_name(current_buf) if string.find(file_name, "term://") ~= nil then icon = " " .. api.nvim_call_function("fnamemodify", { file_name, ":p:t" }) end file_name = api.nvim_call_function("fnamemodify", { file_name, ":p:t" }) if file_name == "" then icon = "" return icon end local icon = icons.deviconTable[file_name] if icon ~= nil then return icon .. space else return "" end end return M
Lua
local bufname = require "sections._bufname" local space = " " local M = {} function M.is_buffer_modified() local file = bufname.get_buffer_name() if file == " startify " then return "" end -- exception check if vim.bo.modifiable and vim.bo.modified then return "+" .. space end return "" end return M
Lua
local M = {} local space = " " function M.get_buffer_name() --> IF We are in a buffer such as terminal or startify with no filename just display the buffer 'type' i.e "startify" local filename = vim.fn.expand "%:t" local filetype = vim.bo.ft --> Get vim filetype using nvim api if filename ~= "" then --> IF filetype empty i.e in a terminal buffer etc, return name of buffer (filetype) return filename .. space else if filetype ~= "" then return filetype .. space else return "" --> AFAIK buffers tested have types but just incase. end end end return M
Lua
local M = {} local vim = vim local space = " " local function file_size(file) local size = vim.fn.getfsize(file) if size == 0 or size == -1 or size == -2 then return "" end if size < 1024 then size = size .. "B" elseif size < 1024 * 1024 then size = string.format("%d", size / 1024) .. "KB" elseif size < 1024 * 1024 * 1024 then size = string.format("%d", size / 1024 / 1024) .. "MB" else size = string.format("%d", size / 1024 / 1024 / 1024) .. "GB" end return size .. space end function M.get_file_size() local file = vim.fn.expand "%:p" if string.len(file) == 0 then return "" end return file_size(file) end return M
Lua
local M = {} local git_branch local space = " " local sep = package.config:sub(1, 1) local function find_git_dir() local file_dir = vim.fn.expand "%:p:h" .. ";" local git_dir = vim.fn.finddir(".git", file_dir) local git_file = vim.fn.findfile(".git", file_dir) if #git_file > 0 then git_file = vim.fn.fnamemodify(git_file, ":p") end if #git_file > #git_dir then -- separate git-dir or submodule is used local file = io.open(git_file) git_dir = file:read() git_dir = git_dir:match "gitdir: (.+)$" file:close() -- submodule / relative file path if git_dir:sub(1, 1) ~= sep and not git_dir:match "^%a:.*$" then git_dir = git_file:match "(.*).git" .. git_dir end end return git_dir end local function get_git_head(head_file) local f_head = io.open(head_file) if f_head then local HEAD = f_head:read() f_head:close() local branch = HEAD:match "ref: refs/heads/(.+)$" if branch then git_branch = branch else git_branch = HEAD:sub(1, 6) end end return nil end -- event watcher to watch head file local file_changed = vim.loop.new_fs_event() local function watch_head() file_changed:stop() local git_dir = find_git_dir() if #git_dir > 0 then local head_file = git_dir .. sep .. "HEAD" get_git_head(head_file) file_changed:start( head_file, {}, vim.schedule_wrap(function() -- reset file-watch watch_head() end) ) else git_branch = nil end end -- returns the git_branch value to be shown on statusline function M.branch() if not git_branch or #git_branch == 0 then return "" end local icon = "" return icon .. space .. git_branch .. space end -- run watch head on load so branch is present when component is loaded watch_head() return M
Lua
local M = {} local space = " " local vim = vim function M.current_function() local lsp_function = vim.b.lsp_current_function if lsp_function == nil then return "" end return lsp_function end -- icons function M.diagnostics() local diagnostics = "" local e = vim.lsp.diagnostic.get_count(0, [[Error]]) local w = vim.lsp.diagnostic.get_count(0, [[Warning]]) local i = vim.lsp.diagnostic.get_count(0, [[Information]]) local h = vim.lsp.diagnostic.get_count(0, [[Hint]]) diagnostics = e ~= 0 and diagnostics .. " " .. e .. space or diagnostics diagnostics = w ~= 0 and diagnostics .. " " .. w .. space or diagnostics diagnostics = i ~= 0 and diagnostics .. "𝒊 " .. i .. space or diagnostics diagnostics = h ~= 0 and diagnostics .. " " .. h .. space or diagnostics return diagnostics end local function format_messages(messages) local result = {} local spinners = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" } local ms = vim.loop.hrtime() / 1000000 local frame = math.floor(ms / 120) % #spinners local i = 1 for _, msg in pairs(messages) do -- Only display at most 2 progress messages at a time to avoid clutter if i < 3 then table.insert(result, (msg.percentage or 0) .. "%% " .. (msg.title or "")) i = i + 1 end end return table.concat(result, " ") .. " " .. spinners[frame + 1] end -- REQUIRES LSP function M.lsp_progress() local messages = vim.lsp.util.get_progress_messages() if #messages == 0 then return "" end return format_messages(messages) end -- REQUIRES NVIM LIGHTBULB function M.lightbulb() local has_lightbulb, lightbulb = pcall(require, "nvim-lightbulb") if not has_lightbulb then return "" end if lightbulb.get_status_text() ~= "" then return "" .. space else return "" end end return M
Lua
local M = {} local space = " " function M.signify() if vim.fn.exists "*sy#repo#get_stats" == 0 then return "" end local added, modified, removed = unpack(vim.fn["sy#repo#get_stats"]()) if added == -1 then return "" end local symbols = { "+", "-", "~", } local result = {} local data = { added, removed, modified, } for range = 1, 3 do if data[range] ~= nil and data[range] > 0 then table.insert(result, symbols[range] .. data[range] .. space) end end if result[1] ~= nil then return table.concat(result, "") else return "" end end return M
- Tables
Just contains all the icons and modes I use throughout the statusline
Lua
local M = {} local extensionTable = { -- Exact Match ["gruntfile.coffee"] = "", ["gruntfile.js"] = "", ["gruntfile.ls"] = "", ["gulpfile.coffee"] = "", ["gulpfile.js"] = "", ["gulpfile.ls"] = "", ["mix.lock"] = "", ["dropbox"] = "", [".ds_store"] = "", [".gitconfig"] = "", [".gitignore"] = "", [".gitlab-ci.yml"] = "", [".bashrc"] = "", [".zshrc"] = "", [".vimrc"] = "", [".gvimrc"] = "", ["_vimrc"] = "", ["_gvimrc"] = "", [".bashprofile"] = "", ["favicon.ico"] = "", ["license"] = "", ["node_modules"] = "", ["react.jsx"] = "", ["procfile"] = "", ["dockerfile"] = "", ["docker-compose.yml"] = "", -- Extension ["styl"] = "", ["sass"] = "", ["scss"] = "", ["htm"] = "", ["html"] = "", ["slim"] = "", ["ejs"] = "", ["css"] = "", ["less"] = "", ["md"] = "", ["mdx"] = "", ["markdown"] = "", ["rmd"] = "", ["json"] = "", ["js"] = "", ["mjs"] = "", ["jsx"] = "", ["rb"] = "", ["php"] = "", ["py"] = "", ["pyc"] = "", ["pyo"] = "", ["pyd"] = "", ["coffee"] = "", ["mustache"] = "", ["hbs"] = "", ["conf"] = "", ["ini"] = "", ["yml"] = "", ["yaml"] = "", ["toml"] = "", ["bat"] = "", ["jpg"] = "", ["jpeg"] = "", ["bmp"] = "", ["png"] = "", ["gif"] = "", ["ico"] = "", ["twig"] = "", ["cpp"] = "", ["c++"] = "", ["cxx"] = "", ["cc"] = "", ["cp"] = "", ["c"] = "", ["cs"] = "", ["h"] = "", ["hh"] = "", ["hpp"] = "", ["hxx"] = "", ["hs"] = "", ["lhs"] = "", ["lua"] = "", ["java"] = "", ["sh"] = "", ["fish"] = "", ["bash"] = "", ["zsh"] = "", ["ksh"] = "", ["csh"] = "", ["awk"] = "", ["ps1"] = "", ["ml"] = "λ", ["mli"] = "λ", ["diff"] = "", ["db"] = "", ["sql"] = "", ["dump"] = "", ["clj"] = "", ["cljc"] = "", ["cljs"] = "", ["edn"] = "", ["scala"] = "", ["go"] = "", ["dart"] = "", ["xul"] = "", ["sln"] = "", ["suo"] = "", ["pl"] = "", ["pm"] = "", ["t"] = "", ["rss"] = "", ["f#"] = "", ["fsscript"] = "", ["fsx"] = "", ["fs"] = "", ["fsi"] = "", ["rs"] = "", ["rlib"] = "", ["d"] = "", ["erl"] = "", ["hrl"] = "", ["ex"] = "", ["exs"] = "", ["eex"] = "", ["leex"] = "", ["vim"] = "", ["ai"] = "", ["psd"] = "", ["psb"] = "", ["ts"] = "", ["tsx"] = "", ["jl"] = "", ["pp"] = "", ["vue"] = "﵂", ["elm"] = "", ["swift"] = "", ["xcplayground"] = "", } M.deviconTable = setmetatable(extensionTable, { __index = function(extensionTable, key) local i = string.find(key, "[.*]") if i ~= nil then return extensionTable[string.sub(key, i + 1)] end end, }) return M
Lua
local M = {} M.current_mode = setmetatable({ ["n"] = "<N>", ["no"] = "N·Operator Pending", ["v"] = "<V>", ["V"] = "<V>", ["^V"] = "<V>", ["s"] = "<S>", ["S"] = "S·Line", ["^S"] = "S·Block", ["i"] = "<I>", ["ic"] = "<I>", ["ix"] = "<I>", ["R"] = "<R>", ["Rv"] = "<V·Replace>", ["c"] = "<C>", ["cv"] = "<Vim Ex>", ["ce"] = "<Ex>", ["r"] = "Prompt", ["rm"] = "More", ["r?"] = "Confirm", ["!"] = "Shell", ["t"] = "T", }, { __index = function(_, _) return "V·Block" end, }) return M
- Modules
6. Extra ATTACH
6.1. Hammerspoon
Yabai breaks pretty often, so I use hammerspoon as a wm instead! Heres how its done:
Lua
-- Load in config as modules wm = require("wm") plugins = require("plugins") bindings = require("bindings") -- hide window shadows (doesn't work) hs.window.setShadows(false) -- Load hs on the command line hs.ipc.cliInstall() -- bindings bindings.enabled = { "grid", "hotkeys", "tiling" } -- start/stop modules local modules = { wm, plugins, bindings } hs.fnutils.each(modules, function(module) if module then module.start() end end) -- stop modules on shutdown hs.shutdownCallback = function() hs.fnutils.each(modules, function(module) if module then module.stop() end end) end -- watch config for changes and reload when they occur function reloadConfig(files) doReload = false for _, file in pairs(files) do if file:sub(-4) == ".lua" then doReload = true end end if doReload then hs.reload() end end hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start() hs.notify.new({ title = "hammerspoon", informativeText = "Config reloaded" }):send()
6.1.1. Hhtwm
This is where most of the magit happens. It interfaces with hammerspoon’s abstractions to macOS’s events, and controlls windows. The layout.lua files defines the usual layouts (grid, 3/2, etc)
Lua
-- hhtwm - hackable hammerspoon tiling wm local createLayouts = require("hhtwm.layouts") local spaces = require("hs._asm.undocumented.spaces") local cache = { spaces = {}, layouts = {}, floating = {}, layoutOptions = {} } local module = { cache = cache } local layouts = createLayouts(module) local log = hs.logger.new("hhtwm", "debug") local SWAP_BETWEEN_SCREENS = false local getDefaultLayoutOptions = function() return { mainPaneRatio = 0.5, } end local capitalize = function(str) return str:gsub("^%l", string.upper) end local ternary = function(cond, ifTrue, ifFalse) if cond then return ifTrue else return ifFalse end end local ensureCacheSpaces = function(spaceId) if spaceId and not cache.spaces[spaceId] then cache.spaces[spaceId] = {} end end local getCurrentSpacesIds = function() return spaces.query(spaces.masks.currentSpaces) end local getSpaceId = function(win) local spaceId win = win or hs.window.frontmostWindow() if win ~= nil and win:spaces() ~= nil and #win:spaces() > 0 then spaceId = win:spaces()[1] end return spaceId or spaces.activeSpace() end local getSpacesIdsTable = function() local spacesLayout = spaces.layout() local spacesIds = {} hs.fnutils.each(hs.screen.allScreens(), function(screen) local spaceUUID = screen:spacesUUID() local userSpaces = hs.fnutils.filter(spacesLayout[spaceUUID], function(spaceId) return spaces.spaceType(spaceId) == spaces.types.user end) hs.fnutils.concat(spacesIds, userSpaces or {}) end) return spacesIds end local getAllWindowsUsingSpaces = function() local spacesIds = getSpacesIdsTable() local tmp = {} hs.fnutils.each(spacesIds, function(spaceId) local windows = spaces.allWindowsForSpace(spaceId) hs.fnutils.each(windows, function(win) table.insert(tmp, win) end) end) return tmp end local getScreenBySpaceId = function(spaceId) local spacesLayout = spaces.layout() return hs.fnutils.find(hs.screen.allScreens(), function(screen) local spaceUUID = screen:spacesUUID() return hs.fnutils.contains(spacesLayout[spaceUUID], spaceId) end) end local getCurrentSpacesByScreen = function() local currentSpaces = spaces.query(spaces.masks.currentSpaces) local spacesIds = {} hs.fnutils.each(hs.screen.allScreens(), function(screen) local screenSpaces = screen:spaces() local visibleSpace = hs.fnutils.find(screenSpaces, function(spaceId) return hs.fnutils.contains(currentSpaces, spaceId) end) spacesIds[screen:id()] = visibleSpace end) return spacesIds end -- iterate through cache.spaces to find matching window:id() -- returns foundInAllWindows window, its space, and index in array module.findTrackedWindow = function(win) if not win then return nil, nil, nil end local foundSpaceId, foundWinIndex, foundWin local didFound = false for spaceId, spaceWindows in pairs(cache.spaces) do for winIndex, window in pairs(spaceWindows) do if not didFound then didFound = window:id() == win:id() if didFound then foundSpaceId = spaceId foundWinIndex = winIndex foundWin = window end end end end return foundWin, foundSpaceId, foundWinIndex end module.getLayouts = function() local layoutNames = {} for key in pairs(layouts) do table.insert(layoutNames, key) end return layoutNames end -- sets layout for space id, -- defaults to current windows space module.setLayout = function(layout, spaceId) spaceId = spaceId or getSpaceId() if not spaceId then return end cache.layouts[spaceId] = layout -- remove all floating windows that are on this space, -- so retiling will put them back in the layout -- this allows us to switch back from floating layout and get windows back to layout cache.floating = hs.fnutils.filter(cache.floating, function(win) return win:spaces()[1] ~= spaceId end) module.tile() end -- get layout for space id, priorities: -- 1. already set layout (cache) -- 2. layout selected by setLayout -- 3. layout assigned by tilign.displayLayouts -- 4. if all else fails - 'monocle' module.getLayout = function(spaceId) spaceId = spaceId or getSpaceId() local layout = spaces.layout() local foundScreenUUID for screenUUID, layoutSpaces in pairs(layout) do if not foundScreenUUID then if hs.fnutils.contains(layoutSpaces, spaceId) then foundScreenUUID = screenUUID end end end local screen = hs.fnutils.find(hs.screen.allScreens(), function(screen) return screen:spacesUUID() == foundScreenUUID end) return cache.layouts[spaceId] or (screen and module.displayLayouts and module.displayLayouts[screen:id()]) or (screen and module.displayLayouts and module.displayLayouts[screen:name()]) or "monocle" end -- resbuild cache.layouts table using provided hhtwm.displayLayouts and hhtwm.defaultLayout module.resetLayouts = function() for key in pairs(cache.layouts) do cache.layouts[key] = nil cache.layouts[key] = module.getLayout(key) end end module.resizeLayout = function(resizeOpt) local spaceId = getSpaceId() if not spaceId then return end if not cache.layoutOptions[spaceId] then cache.layoutOptions[spaceId] = getDefaultLayoutOptions() end local calcResizeStep = module.calcResizeStep or function() return 0.1 end local screen = getScreenBySpaceId(spaceId) local step = calcResizeStep(screen) local ratio = cache.layoutOptions[spaceId].mainPaneRatio if not resizeOpt then ratio = 0.5 elseif resizeOpt == "thinner" then ratio = math.max(ratio - step, 0) elseif resizeOpt == "wider" then ratio = math.min(ratio + step, 1) end cache.layoutOptions[spaceId].mainPaneRatio = ratio module.tile() end module.equalizeLayout = function() local spaceId = getSpaceId() if not spaceId then return end if cache.layoutOptions[spaceId] then cache.layoutOptions[spaceId] = getDefaultLayoutOptions() module.tile() end end -- swap windows in direction -- works between screens module.swapInDirection = function(win, direction) win = win or hs.window.frontmostWindow() if module.isFloating(win) then return end local winCmd = "windowsTo" .. capitalize(direction) local ONLY_FRONTMOST = true local STRICT_ANGLE = true local windowsInDirection = cache.filter[winCmd](cache.filter, win, ONLY_FRONTMOST, STRICT_ANGLE) windowsInDirection = hs.fnutils.filter(windowsInDirection, function(testWin) return testWin:isStandard() and not module.isFloating(testWin) end) if #windowsInDirection >= 1 then local winInDirection = windowsInDirection[1] local _, winInDirectionSpaceId, winInDirectionIdx = module.findTrackedWindow(winInDirection) local _, winSpaceId, winIdx = module.findTrackedWindow(win) if hs.fnutils.some({ win, winInDirection, winInDirectionSpaceId, winInDirectionIdx, winSpaceId, winIdx, }, function(_) return _ == nil end) then log.e("swapInDirection error", hs.inspect({ winInDirectionSpaceId, winInDirectionIdx, winSpaceId, winIdx })) return end local winInDirectionScreen = winInDirection:screen() local winScreen = win:screen() -- local winInDirectionFrame = winInDirection:frame() -- local winFrame = win:frame() -- if swapping between screens is disabled, then return early if screen ids differ if not SWAP_BETWEEN_SCREENS and winScreen:id() ~= winInDirectionScreen:id() then return end -- otherwise, move to screen if they differ if winScreen:id() ~= winInDirectionScreen:id() then win:moveToScreen(winInDirectionScreen) winInDirection:moveToScreen(winScreen) end ensureCacheSpaces(winSpaceId) ensureCacheSpaces(winInDirectionSpaceId) -- swap frames -- winInDirection:setFrame(winFrame) -- win:setFrame(winInDirectionFrame) -- swap positions in arrays cache.spaces[winSpaceId][winIdx] = winInDirection cache.spaces[winInDirectionSpaceId][winInDirectionIdx] = win -- ~~no need to retile, assuming both windows were previously tiled!~~ module.tile() end end -- throw window to screen - de-attach and re-attach module.throwToScreen = function(win, direction) win = win or hs.window.frontmostWindow() if module.isFloating(win) then return end local directions = { next = "next", prev = "previous", } if not directions[direction] then log.e("can't throw in direction:", direction) return end local screen = win:screen() local screenInDirection = screen[directions[direction]](screen) if screenInDirection then local _, winSpaceId, winIdx = module.findTrackedWindow(win) if hs.fnutils.some({ winSpaceId, winIdx }, function(_) return _ == nil end) then log.e("throwToScreen error", hs.inspect({ winSpaceId, winIdx })) return end -- remove from tiling so we re-tile that window after it was moved if cache.spaces[winSpaceId] then table.remove(cache.spaces[winSpaceId], winIdx) else log.e("throwToScreen no cache.spaces for space id:", winSpaceId) end -- move window to screen win:moveToScreen(screenInDirection) -- retile to update layouts if hs.window.animationDuration > 0 then hs.timer.doAfter(hs.window.animationDuration * 1.2, module.tile) else module.tile() end end end module.throwToScreenUsingSpaces = function(win, direction) win = win or hs.window.frontmostWindow() if module.isFloating(win) then return end local directions = { next = "next", prev = "previous", } if not directions[direction] then log.e("can't throw in direction:", direction) return end local screen = win:screen() local screenInDirection = screen[directions[direction]](screen) local currentSpaces = getCurrentSpacesByScreen() local throwToSpaceId = currentSpaces[screenInDirection:id()] if not throwToSpaceId then log.e("no space to throw to") return end local _, winSpaceId, winIdx = module.findTrackedWindow(win) if hs.fnutils.some({ winSpaceId, winIdx }, function(_) return _ == nil end) then log.e("throwToScreenUsingSpaces error", hs.inspect({ winSpaceId, winIdx })) return end -- remove from tiling so we re-tile that window after it was moved if cache.spaces[winSpaceId] then table.remove(cache.spaces[winSpaceId], winIdx) else log.e("throwToScreenUsingSpaces no cache.spaces for space id:", winSpaceId) end local newX = screenInDirection:frame().x local newY = screenInDirection:frame().y spaces.moveWindowToSpace(win:id(), throwToSpaceId) win:setTopLeft(newX, newY) module.tile() end -- throw window to space, indexed module.throwToSpace = function(win, spaceIdx) if not win then log.e("throwToSpace tried to throw nil window") return false end local spacesIds = getSpacesIdsTable() local spaceId = spacesIds[spaceIdx] if not spaceId then log.e("throwToSpace tried to move to non-existing space", spaceId, hs.inspect(spacesIds)) return false end local targetScreen = getScreenBySpaceId(spaceId) local targetScreenFrame = targetScreen:frame() if module.isFloating(win) then -- adjust frame for new screen offset local newX = win:frame().x - win:screen():frame().x + targetScreen:frame().x local newY = win:frame().y - win:screen():frame().y + targetScreen:frame().y -- move to space spaces.moveWindowToSpace(win:id(), spaceId) -- ensure window is visible win:setTopLeft(newX, newY) return true end local _, winSpaceId, winIdx = module.findTrackedWindow(win) if hs.fnutils.some({ winSpaceId, winIdx }, function(_) return _ == nil end) then log.e("throwToSpace error", hs.inspect({ winSpaceId, winIdx })) return false end -- remove from tiling so we re-tile that window after it was moved if cache.spaces[winSpaceId] then table.remove(cache.spaces[winSpaceId], winIdx) else log.e("throwToSpace no cache.spaces for space id:", winSpaceId) end -- move to space spaces.moveWindowToSpace(win:id(), spaceId) -- ensure window is visible win:setTopLeft(targetScreenFrame.x, targetScreenFrame.y) -- retile when finished module.tile() return true end -- check if window is floating module.isFloating = function(win) local trackedWin, _, _ = module.findTrackedWindow(win) local isTrackedAsTiling = trackedWin ~= nil if isTrackedAsTiling then return false end local isTrackedAsFloating = hs.fnutils.find(cache.floating, function(floatingWin) return floatingWin:id() == win:id() end) -- if window is not floating and not tiling, then default to floating? if isTrackedAsFloating == nil and trackedWin == nil then return true end return isTrackedAsFloating ~= nil end -- toggle floating state of window module.toggleFloat = function(win) win = win or hs.window.frontmostWindow() if not win then return end if module.isFloating(win) then local spaceId = win:spaces()[1] local foundIdx for index, floatingWin in pairs(cache.floating) do if not foundIdx then if floatingWin:id() == win:id() then foundIdx = index end end end ensureCacheSpaces(spaceId) table.insert(cache.spaces[spaceId], win) table.remove(cache.floating, foundIdx) else local foundWin, winSpaceId, winIdx = module.findTrackedWindow(win) if cache.spaces[winSpaceId] then table.remove(cache.spaces[winSpaceId], winIdx) else log.e("window made floating without previous :space()", hs.inspect(foundWin)) end table.insert(cache.floating, win) end -- update tiling module.tile() end -- internal function for deciding if window should float when recalculating tiling local shouldFloat = function(win) -- if window is in tiling cache, then it is not floating local inTilingCache, _, _ = module.findTrackedWindow(win) if inTilingCache then return false end -- if window is already tracked as floating, then leave it be local isTrackedAsFloating = hs.fnutils.find(cache.floating, function(floatingWin) return floatingWin:id() == win:id() end) ~= nil if isTrackedAsFloating then return true end -- otherwise detect if window should be floated/tiled return not module.detectTile(win) end -- tile windows - combine caches with current state, and apply layout module.tile = function() -- ignore tiling if we're doing something with a mouse if #hs.mouse.getButtons() ~= 0 then return end -- this allows us to have tabs and do proper tiling! local tilingWindows = {} local floatingWindows = {} local currentSpaces = getCurrentSpacesIds() local allWindows = hs.window.allWindows() -- local allWindowsFilter = cache.filter:getWindows() -- local allWindowsSpaces = getAllWindowsUsingSpaces() -- log.d('allWindows', hs.inspect(allWindows)) -- log.d('allWindowsFilter', hs.inspect(allWindowsFilter)) hs.fnutils.each(allWindows or {}, function(win) -- we don't care about minimized or fullscreen windows if win:isMinimized() or win:isFullscreen() then return end -- we also don't care about special windows that have no spaces if not win:spaces() or #win:spaces() == 0 then return end if shouldFloat(win) then table.insert(floatingWindows, win) else table.insert(tilingWindows, win) end end) -- add new tiling windows to cache hs.fnutils.each(tilingWindows, function(win) if not win or #win:spaces() == 0 then return end local spaces = win:spaces() local spaceId = spaces[1] local tmp = cache.spaces[spaceId] or {} local trackedWin, trackedSpaceId, _ = module.findTrackedWindow(win) -- log.d('update cache.spaces', hs.inspect({ -- win = win, -- spaces = spaces, -- spaceId = spaceId, -- trackedWin = trackedWin or 'none', -- trackedSpaceId = trackedSpaceId or 'none', -- shouldInsert = not trackedWin or trackedSpaceId ~= spaceId -- })) -- window is "new" if it's not in cache at all, or if it changed space if not trackedWin or trackedSpaceId ~= spaceId then -- table.insert(tmp, 1, win) table.insert(tmp, win) end cache.spaces[spaceId] = tmp end) -- clean up tiling cache hs.fnutils.each(currentSpaces, function(spaceId) local spaceWindows = cache.spaces[spaceId] or {} for i = #spaceWindows, 1, -1 do -- window exists in cache if there's spaceId and windowId match local existsOnScreen = hs.fnutils.find(tilingWindows, function(win) return win:id() == spaceWindows[i]:id() and win:spaces()[1] == spaceId end) -- window is duplicated (why?) if it's tracked more than once -- this shouldn't happen, but helps for now... local duplicateIdx = 0 for j = 1, #spaceWindows do if spaceWindows[i]:id() == spaceWindows[j]:id() and i ~= j then duplicateIdx = j end end if duplicateIdx > 0 then log.e( "duplicate idx", hs.inspect({ i = i, duplicateIdx = duplicateIdx, spaceWindows = spaceWindows, }) ) end if not existsOnScreen or duplicateIdx > 0 then table.remove(spaceWindows, i) else i = i + 1 end end cache.spaces[spaceId] = spaceWindows end) -- add new windows to floating cache hs.fnutils.each(floatingWindows, function(win) if not module.isFloating(win) then table.insert(cache.floating, win) end end) -- clean up floating cache cache.floating = hs.fnutils.filter(cache.floating, function(cacheWin) return hs.fnutils.find(floatingWindows, function(win) return cacheWin:id() == win:id() end) end) -- apply layout window-by-window local moveToFloat = {} hs.fnutils.each(currentSpaces, function(spaceId) local spaceWindows = cache.spaces[spaceId] or {} hs.fnutils.each(hs.screen.allScreens(), function(screen) local screenWindows = hs.fnutils.filter(spaceWindows, function(win) return win:screen():id() == screen:id() end) local layoutName = module.getLayout(spaceId) if not layoutName or not layouts[layoutName] then log.e("layout doesn't exist: " .. layoutName) else for index, window in pairs(screenWindows) do local frame = layouts[layoutName]( window, screenWindows, screen, index, cache.layoutOptions[spaceId] or getDefaultLayoutOptions() ) -- only set frame if returned, -- this allows for layout to decide if window should be floating if frame then window:setFrame(frame) else table.insert(moveToFloat, window) end end end end) end) hs.fnutils.each(moveToFloat, function(win) local _, spaceId, winIdx = module.findTrackedWindow(win) table.remove(cache.spaces[spaceId], winIdx) table.insert(cache.floating, win) end) end -- tile detection: -- 1. test tiling.filters if exist -- 2. check if there's fullscreen button -> yes = tile, no = float module.detectTile = function(win) local app = win:application():name() local role = win:role() local subrole = win:subrole() local title = win:title() if module.filters then local foundMatch = hs.fnutils.find(module.filters, function(obj) local appMatches = ternary(obj.app ~= nil and app ~= nil, string.match(app, obj.app or ""), true) local titleMatches = ternary(obj.title ~= nil and title ~= nil, string.match(title, obj.title or ""), true) local roleMatches = ternary(obj.role ~= nil, obj.role == role, true) local subroleMatches = ternary(obj.subrole ~= nil, obj.subrole == subrole, true) return appMatches and titleMatches and roleMatches and subroleMatches end) if foundMatch then return foundMatch.tile end end local shouldTileDefault = hs.axuielement.windowElement(win):isAttributeSettable("AXSize") return shouldTileDefault end -- mostly for debugging module.reset = function() cache.spaces = {} cache.layouts = {} cache.floating = {} module.tile() end local loadSettings = function() -- load from cache local jsonTilingCache = hs.settings.get("hhtwm.tilingCache") local jsonFloatingCache = hs.settings.get("hhtwm.floatingCache") log.d("reading from hs.settings") log.d("hhtwm.tilingCache", jsonTilingCache) log.d("hhtwm.floatingCache", jsonFloatingCache) -- all windows from window filter local allWindows = getAllWindowsUsingSpaces() local findWindowById = function(winId) return hs.fnutils.find(allWindows, function(win) return win:id() == winId end) end -- decode tiling cache if jsonTilingCache then local tilingCache = hs.json.decode(jsonTilingCache) local spacesIds = getSpacesIdsTable() hs.fnutils.each(tilingCache, function(obj) -- we don't care about spaces that no longer exist if hs.fnutils.contains(spacesIds, obj.spaceId) then cache.spaces[obj.spaceId] = {} cache.layouts[obj.spaceId] = obj.layout cache.layoutOptions[obj.spaceId] = obj.layoutOptions hs.fnutils.each(obj.windowIds, function(winId) local win = findWindowById(winId) log.d("restoring (spaceId, windowId, window)", obj.spaceId, winId, win) if win then table.insert(cache.spaces[obj.spaceId], win) end end) end end) end -- decode floating cache if jsonFloatingCache then local floatingCache = hs.json.decode(jsonFloatingCache) hs.fnutils.each(floatingCache, function(winId) local win = hs.window.find(winId) -- we don't care about windows that no longer exist if win then table.insert(cache.floating, win) end end) end log.d("read from hs.settings") log.d("cache.spaces", hs.inspect(cache.spaces)) log.d("cache.floating", hs.inspect(cache.floating)) end local saveSettings = function() local tilingCache = {} local floatingCache = hs.fnutils.map(cache.floating, function(win) return win:id() end) for spaceId, spaceWindows in pairs(cache.spaces) do -- only save spaces with windows on them if #spaceWindows > 0 then local tmp = {} for _, window in pairs(spaceWindows) do log.d("storing (spaceId, windowId, window)", spaceId, window:id(), window) table.insert(tmp, window:id()) end table.insert(tilingCache, { spaceId = spaceId, layout = module.getLayout(spaceId), layoutOptions = cache.layoutOptions[spaceId], windowIds = tmp, }) end end local jsonTilingCache = hs.json.encode(tilingCache) local jsonFloatingCache = hs.json.encode(floatingCache) log.d("storing to hs.settings") log.d("hhtwm.tiling", jsonTilingCache) log.d("hhtwm.floating", jsonFloatingCache) hs.settings.set("hhtwm.tilingCache", jsonTilingCache) hs.settings.set("hhtwm.floatingCache", jsonFloatingCache) end module.start = function() -- discover windows on spaces as soon as possible -- hs.window.filter.forceRefreshOnSpaceChange = true -- start window filter cache.filter = hs.window.filter.new():setDefaultFilter():setOverrideFilter({ visible = true, -- only allow visible windows fullscreen = false, -- ignore fullscreen windows -- currentSpace = true, -- only windows on current space allowRoles = { "AXStandardWindow" }, }) -- :setSortOrder(hs.window.filter.sortByCreated) -- load window/floating status from saved state loadSettings() -- retile automatically when windows change cache.filter:subscribe({ hs.window.filter.windowsChanged }, module.tile) -- update on screens change cache.screenWatcher = hs.screen.watcher.new(module.tile):start() -- tile on start module.tile() end module.stop = function() -- store cache so we persist layouts between restarts saveSettings() -- stop filter cache.filter:unsubscribeAll() -- stop watching screens cache.screenWatcher:stop() end return module
Lua
-- hhtwm layouts return function(hhtwm) local layouts = {} local getInsetFrame = function(screen) local screenFrame = screen:fullFrame() local screenMargin = hhtwm.screenMargin or { top = 0, bottom = 0, right = 0, left = 0 } return { x = screenFrame.x + screenMargin.left, y = screenFrame.y + screenMargin.top, w = screenFrame.w - (screenMargin.left + screenMargin.right), h = screenFrame.h - (screenMargin.top + screenMargin.bottom), } end layouts["floating"] = function() return nil end layouts["monocle"] = function(_, _, screen) local margin = hhtwm.margin or 0 local insetFrame = getInsetFrame(screen) local frame = { x = insetFrame.x + margin / 2, y = insetFrame.y + margin / 2, w = insetFrame.w - margin, h = insetFrame.h - margin, } return frame end layouts["main-left"] = function(window, windows, screen, index, layoutOptions) if #windows == 1 then return layouts["main-center"](window, windows, screen, index, layoutOptions) end local margin = hhtwm.margin or 0 local insetFrame = getInsetFrame(screen) local frame = { x = insetFrame.x, y = insetFrame.y, w = 0, h = 0, } if index == 1 then frame.x = frame.x + margin / 2 frame.y = frame.y + margin / 2 frame.h = insetFrame.h - margin frame.w = insetFrame.w * layoutOptions.mainPaneRatio - margin else local divs = #windows - 1 local h = insetFrame.h / divs frame.h = h - margin frame.w = insetFrame.w * (1 - layoutOptions.mainPaneRatio) - margin frame.x = frame.x + insetFrame.w * layoutOptions.mainPaneRatio + margin / 2 frame.y = frame.y + h * (index - 2) + margin / 2 end return frame end layouts["main-right"] = function(window, windows, screen, index, layoutOptions) if #windows == 1 then return layouts["main-center"](window, windows, screen, index, layoutOptions) end local margin = hhtwm.margin or 0 local insetFrame = getInsetFrame(screen) local frame = { x = insetFrame.x, y = insetFrame.y, w = 0, h = 0, } if index == 1 then frame.x = frame.x + insetFrame.w * layoutOptions.mainPaneRatio + margin / 2 frame.y = frame.y + margin / 2 frame.h = insetFrame.h - margin frame.w = insetFrame.w * (1 - layoutOptions.mainPaneRatio) - margin else local divs = #windows - 1 local h = insetFrame.h / divs frame.x = frame.x + margin / 2 frame.y = frame.y + h * (index - 2) + margin / 2 frame.w = insetFrame.w * layoutOptions.mainPaneRatio - margin frame.h = h - margin end return frame end layouts["main-center"] = function(window, windows, screen, index, layoutOptions) local insetFrame = getInsetFrame(screen) local margin = hhtwm.margin or 0 local mainColumnWidth = insetFrame.w * layoutOptions.mainPaneRatio + margin / 2 if index == 1 then return { x = insetFrame.x + (insetFrame.w - mainColumnWidth) / 2 + margin / 2, y = insetFrame.y + margin / 2, w = mainColumnWidth - margin, h = insetFrame.h - margin, } end local frame = { x = insetFrame.x, y = 0, w = (insetFrame.w - mainColumnWidth) / 2 - margin, h = 0, } if (index - 1) % 2 == 0 then local divs = math.floor((#windows - 1) / 2) local h = insetFrame.h / divs frame.x = frame.x + margin / 2 frame.h = h - margin frame.y = insetFrame.y + h * math.floor(index / 2 - 1) + margin / 2 else local divs = math.ceil((#windows - 1) / 2) local h = insetFrame.h / divs frame.x = frame.x + (insetFrame.w - frame.w - margin) + margin / 2 frame.h = h - margin frame.y = insetFrame.y + h * math.floor(index / 2 - 1) + margin / 2 end return frame end layouts["tabbed-left"] = function(window, windows, screen, index, layoutOptions) if #windows == 1 then return layouts["main-center"](window, windows, screen, index, layoutOptions) end local margin = hhtwm.margin or 0 local insetFrame = getInsetFrame(screen) local frame = { x = insetFrame.x, y = insetFrame.y, w = 0, h = 0, } if index == 1 then frame.x = frame.x + insetFrame.w * layoutOptions.mainPaneRatio + margin / 2 frame.y = frame.y + margin / 2 frame.w = insetFrame.w * (1 - layoutOptions.mainPaneRatio) - margin frame.h = insetFrame.h - margin else frame.x = frame.x + margin / 2 frame.y = frame.y + margin / 2 frame.w = insetFrame.w * layoutOptions.mainPaneRatio - margin frame.h = insetFrame.h - margin end return frame end layouts["tabbed-right"] = function(window, windows, screen, index, layoutOptions) if #windows == 1 then return layouts["main-center"](window, windows, screen, index, layoutOptions) end local margin = hhtwm.margin or 0 local insetFrame = getInsetFrame(screen) local frame = { x = insetFrame.x, y = insetFrame.y, w = 0, h = 0, } if index == 1 then frame.x = frame.x + margin / 2 frame.y = frame.y + margin / 2 frame.w = insetFrame.w * layoutOptions.mainPaneRatio - margin frame.h = insetFrame.h - margin else frame.x = frame.x + insetFrame.w * layoutOptions.mainPaneRatio + margin / 2 frame.y = frame.y + margin / 2 frame.w = insetFrame.w * (1 - layoutOptions.mainPaneRatio) - margin frame.h = insetFrame.h - margin end return frame end -- TODO -- layouts["stacking-columns"] = function(window, windows, screen, index, layoutOptions) -- return nil -- end return layouts end
6.1.2. Plugins
With that, we need the caffiene plugin, which this file handles configuring
Lua
local module = { cache = cache } local hyper = { "cmd", "ctrl", "shift" } module.start = function() hs.loadSpoon("SpoonInstall") spoon.SpoonInstall.use_syncinstall = true Install = spoon.SpoonInstall Install:andUse("Caffeine", { start = false, hotkeys = { toggle = { hyper, "1" }, }, }) end module.stop = function() end return module
6.1.3. Bindings
No use having the logic for a WM if we can’t use it. lets define some bindings to help with that
Lua
local cache = {} local module = { cache = cache } -- modifiers in use: -- * cltr+alt: move focus between windows -- * ctrl+shift: do things to windows -- * ultra: custom/global bindings module.start = function() hs.fnutils.each(bindings.enabled, function(binding) cache[binding] = require("bindings." .. binding) cache[binding].start() end) end module.stop = function() hs.fnutils.each(cache, function(binding) binding.stop() end) end return module
Lua
local module = { cache = cache } module.start = function() local mash = { "ctrl", "cmd" } hs.window.animationDuration = 0 function bindKey(key, fn) hs.hotkey.bind(mash, key, fn) end positions = { maximized = { x = 0, y = 0, w = 1, h = 1 }, centered = { x = 0.17, y = 0.08, w = 0.66, h = 0.85 }, center = { x = 0.1, y = 0.05, w = 0.77, h = 0.88 }, left34 = { x = 0, y = 0, w = 0.34, h = 1 }, left50 = hs.layout.left50, left66 = { x = 0, y = 0, w = 0.66, h = 1 }, left70 = hs.layout.left70, right30 = hs.layout.right30, right34 = { x = 0.66, y = 0, w = 0.34, h = 1 }, right50 = hs.layout.right50, right66 = { x = 0.34, y = 0, w = 0.66, h = 1 }, upper50 = { x = 0, y = 0, w = 1, h = 0.5 }, upper50Left50 = { x = 0, y = 0, w = 0.5, h = 0.5 }, upper50Right15 = { x = 0.85, y = 0, w = 0.15, h = 0.5 }, upper50Right30 = { x = 0.7, y = 0, w = 0.3, h = 0.5 }, upper50Right50 = { x = 0.5, y = 0, w = 0.5, h = 0.5 }, lower50 = { x = 0, y = 0.5, w = 1, h = 0.5 }, lower50Left50 = { x = 0, y = 0.5, w = 0.5, h = 0.5 }, lower50Right50 = { x = 0.5, y = 0.5, w = 0.5, h = 0.5 }, chat = { x = 0.5, y = 0, w = 0.35, h = 0.5 }, } -- Grid grid = { { key = "h", units = { positions.left50, positions.left66, positions.left34 } }, { key = "j", units = { positions.lower50 } }, { key = "k", units = { positions.upper50 } }, { key = "l", units = { positions.right50, positions.right66, positions.right34 } }, { key = "u", units = { positions.upper50Left50 } }, { key = "o", units = { positions.upper50Right50 } }, { key = "i", units = { positions.centered, positions.center, positions.maximized } }, { key = ",", units = { positions.lower50Left50 } }, { key = ".", units = { positions.lower50Right50 } }, { key = "f", units = { positions.maximized } }, } hs.fnutils.each(grid, function(entry) bindKey(entry.key, function() local units = entry.units local screen = hs.screen.mainScreen() local window = hs.window.focusedWindow() local windowGeo = window:frame() local index = 0 hs.fnutils.find(units, function(unit) index = index + 1 local geo = hs.geometry.new(unit):fromUnitRect(screen:frame()):floor() return windowGeo:equals(geo) end) if index == #units then index = 0 end currentLayout = null window:moveToUnit(units[index + 1]) end) end) --Layouts -- hs.hotkey.bind(mash_shift, 'j', function() -- hs.layout.apply({ -- {"Firefox", nil, screen, positions.right50, nil, nil}, -- {"iTerm2", nil, screen, positions.left50, nil, nil} -- }) -- end) -- -- hs.hotkey.bind(mash_shift, 'k', function() -- hs.layout.apply({ -- {"Firefox", nil, screen, positions.center, nil, nil}, -- {"iTerm2", nil, screen, positions.center, nil, nil} -- }) -- end) local function testing(app, eventType, state) -- hs.alert(app) if app == "iTerm" then local maximized = { x = 0, y = 0, w = 1, h = 1 } local windowGeo = window:frame() local screen = hs.screen.mainScreen() local geo = hs.geometry.new(maximized):fromUnitRect(screen:frame()):floor() return windowGeo:equals(geo) end end local setItermSize = hs.application.watcher.new(testing) setItermSize:start() end return module
Lua
local module = { cache = cache } local hyper = { "cmd", "ctrl", "shift" } module.start = function() --------------------------------------------------------- -- App Hotkeys --------------------------------------------------------- hs.hotkey.bind(hyper, "J", function() hs.application.launchOrFocus("Alacritty") end) hs.hotkey.bind(hyper, "K", function() hs.application.launchOrFocus("Firefox") end) hs.hotkey.bind(hyper, "T", function() hs.application.launchOrFocus("Transmit") end) hs.hotkey.bind(hyper, "M", function() hs.application.launchOrFocus("Mail") end) hs.hotkey.bind(hyper, "G", function() hs.application.launchOrFocus("Tower") end) --------------------------------------------------------- -- ON-THE-FLY KEYBIND --------------------------------------------------------- -- Temporarily bind an application to be toggled by the V key -- useful for once-in-a-while applications like Preview local boundApplication = nil hs.hotkey.bind(hyper, "C", function() local appName = hs.window.focusedWindow():application():title() if boundApplication then boundApplication:disable() end boundApplication = hs.hotkey.bind(hyper, "V", function() hs.application.launchOrFocus(appName) end) -- https://github.com/Hammerspoon/hammerspoon/issues/184#issuecomment-102835860 boundApplication:disable() boundApplication:enable() hs.alert(string.format('Binding: "%s" => hyper + V', appName)) end) --------------------------------------------------------- -- Simulate arrow keys with Hyper --------------------------------------------------------- -- https://stackoverflow.com/questions/41025343/when-remapping-hyper-key-caps-lock-to-f18-with-hammerspoon-is-it-possible-to-us arrowKey = function(arrow, modifiers) local event = require("hs.eventtap").event event.newKeyEvent(modifiers, string.lower(arrow), true):post() event.newKeyEvent(modifiers, string.lower(arrow), false):post() end hs.hotkey.bind(hyper, "N", function() arrowKey("DOWN") end) hs.hotkey.bind(hyper, "P", function() arrowKey("UP") end) --------------------------------------------------------- -- Misc --------------------------------------------------------- hs.hotkey.bind(hyper, "X", function() hs.openConsole() hs.focus() end) -- Reload config hs.hotkey.bind(hyper, "R", function() hs.reload() end) end return module
Lua
local module = {} local hhtwm = wm.cache.hhtwm local keys = { "ctrl", "cmd" } hs.window.animationDuration = 0 module.start = function() local bind = function(key, fn) hs.hotkey.bind(keys, key, fn, nil, fn) end -- toggle [f]loat bind("w", function() local win = hs.window.frontmostWindow() if not win then return end hhtwm.toggleFloat(win) if hhtwm.isFloating(win) then hs.grid.center(win) end highlightWindow() end) -- bind('b', function() hhtwm.setLayout('cards') end) bind("]", function() wm.cycleLayout() end) -- [r]eset bind("r", hhtwm.reset) -- re[t]ile bind("t", hhtwm.tile) -- [e]qualize bind("e", hhtwm.equalizeLayout) -- toggle [z]oom window bind("z", function() local win = hs.window.frontmostWindow() if not hhtwm.isFloating(win) then hhtwm.toggleFloat(win) hs.grid.maximizeWindow(win) else hhtwm.toggleFloat(win) end highlightWindow() end) -- throw window to space (and move) for n = 0, 9 do local idx = tostring(n) -- important: use this with onKeyReleased, not onKeyPressed hs.hotkey.bind(keys, idx, nil, function() local win = hs.window.focusedWindow() -- if there's no focused window, just move to that space if not win then hs.eventtap.keyStroke({ "ctrl" }, idx) return end local isFloating = hhtwm.isFloating(win) local success = hhtwm.throwToSpace(win, n) -- if window switched space, then follow it (ctrl + 0..9) and focus if success then hs.eventtap.keyStroke({ "ctrl" }, idx) -- retile and re-highlight window after we switch space hs.timer.doAfter(0.05, function() if not isFloating then hhtwm.tile() end highlightWindow(win) end) end end) end end module.stop = function() end return module
6.2. Wallpapers
Wallpapers are in ./extra/wallpapers/ Some of my favorites: Note: fix later, using links from github
TODO: Attach from github