December 2025

Really trying to get my head around vim variables and schenanegins

ok so g: is for vim variables? like

:echo g:colors_name

like when we set variables in nvim setings! like

vim.g.mapleader = " "

The vim equivalent of setting these variables would be

let g:loaded_netrw = 1

Ahhhh set, this is where my confusion comes from between let which is for setting options! (see :h :set)

Getting confused again.... :let g:loaded_netw = 1 is setting the value of the global variable loaded_netw. Likewise

God... ok

  • g:colors_name is a global variable
  • let accesses variables
  • g: and v: are types of variables
  • vim.opt is an option
  • set accesses options

Ok, asked gemini to do me a table to help clear this up!

FeatureGlobal Variables (g:)Editor Options (opt)
DescriptionCustom data used by plugins or themes.Built-in settings hard-coded into the engine.
AnalogyA sticky note left for a plugin to read.A physical switch or dial on the machine itself.
Exampleg:colors_name (Current theme name)number (Show line numbers in gutter)

Command Cheat Sheet (Get & Set)

ActionGlobal Variable (Example: colors_name)Editor Option (Example: number)
Lua (Set)vim.g.colors_name = "gruvbox"vim.opt.number = true
Lua (Get)print(vim.g.colors_name)print(vim.opt.number:get())
Vimscript (Set)let g:colors_name = "gruvbox"set number
Vimscript (Get)echo g:colors_nameset number?

And now for buffers etc

ActionGlobal Scope (Everywhere)Buffer Scope (Current File Only)
ConceptApplies to the entire editor session.Applies only to the currently open file/tab.
Variable Prefixg: (Vim) / vim.g (Lua)b: (Vim) / vim.b (Lua)
Option Objectvim.optvim.bo
Lua Set Variablevim.g.my_var = 1vim.b.my_var = 1
Lua Set Optionvim.opt.scrolloff = 8vim.bo.shiftwidth = 2
Vim Set Variablelet g:my_var = 1let b:my_var = 1
Vim Set Optionset scrolloff=8setlocal shiftwidth=2

And globals

Lua Globals (_G) vs. Vim Globals (vim.g)

FeatureVim Global (vim.g)Lua Global (_G)
What it isA bridge to the Vimscript engine.The global namespace for the Lua language.
Primary UseConfiguring plugins (e.g., markdown-preview).defining functions callable from Vimscript (v:lua).
VisibilitySeen by .vim files and older plugins.Seen by all .lua files and the v:lua bridge.
Set Commandvim.g.my_var = 1_G.my_func = function() ... end
Get Commandprint(vim.g.my_var)_G.my_func()
Vimscript Accessecho g:my_varcall v:lua.my_func()

Summary of the "Big Three"

  • vim.opt: Switches/knobs for the Editor (Line numbers, tabs).
  • vim.g: Settings for Plugins (Theme names, enable/disable flags).
  • _G: Logic for Functions (Your custom code that needs to run everywhere).

Ok so.... In a lua console

_G.pls = function()
  print("hello?")
  return "no output :("
end

Then can do EITHER :lua print(pls()) or :lua print(_G.pls()) as _G is the global namespace!

Then, the equivalent in vim is

notice that print() does not appear, as it goes to message history? vim.notify WILL print however

This STILL doesn't make sense to me - but I guess that the lua print() is only really for debugging? In general an API show either log to a file or notify?

I found the docs here where it says lua print() redirects its output to the nvim message area?

Ok well the various types of printing in vim and lua are frying my brain a bit - especially since print() in lua claims to print to the message area but... does not? well gemini (tried) to explain

MethodCommandBehaviorPersistence
Lua Printprint("msg")Sends to msg-layerOften overwritten instantly
Vim Cmdvim.cmd('echo "msg"')Standard Vim echoStays until next keypress
Vim APIvim.api.nvim_echo(...)Chunked & HighlightedHigh (configured via args)
Notifyvim.notify("msg")General notificationDepends on UI (popup or cmd)

vim.notify() is fairly modern so that does make sense honestly. its the others

Ok NEVER MIND - it was because i was in the lua console - i assume print() is found to a different messages area or something

xdg-open over SSH (socat forwarding)

The complete flow (step by step)

  1. Local machine: systemd starts open-forward.service on login

    • Runs: socat UNIX-LISTEN:/tmp/open-forward.sock,fork EXEC:"xargs xdg-open"
    • Creates socket at /tmp/open-forward.sock, waiting for connections
  2. You SSH to remote: ssh user@remote

    • SSH reads ~/.ssh/config, sees RemoteForward /tmp/open-forward-%r.sock /tmp/open-forward.sock
    • %r expands to remote username (e.g., joel)
    • SSH creates /tmp/open-forward-joel.sock on remote, tunneled back to local socket
  3. On remote: something calls xdg-open https://example.com

    • Shell finds ~/.local/bin/xdg-open first (due to PATH ordering)
    • Our wrapper script runs instead of system xdg-open
  4. Wrapper script checks: [ -S "/tmp/open-forward-$USER.sock" ]

    • Socket exists? → forward the URL through it
    • No socket? → fallback to system /run/current-system/sw/bin/xdg-open
  5. URL flows through tunnel:

    • printf '%s' "https://example.com" | socat - UNIX-CONNECT:/tmp/open-forward-joel.sock
    • Data goes: remote socket → SSH tunnel → local socket
  6. Local socat receives URL:

    • Pipes it to xargs xdg-open
    • Your local browser opens the URL

SSH config tokens

When you run ssh joel@remote-server:

  • %r = joel (remote username - who you're logging in AS)
  • %u = your local username (on machine running ssh)
  • %h = remote-server (remote hostname)

Since RemoteForward creates the socket on the REMOTE machine, %r matches $USER there.

Concrete example: which socket name is used where?

Setup:

  • Machine A (local laptop): logged in as joelyboy
  • Machine B (remote server): will SSH in as test-user
MACHINE A (local)                        MACHINE B (remote)
logged in as: joelyboy                   logging in as: test-user

~/.ssh/config says:
RemoteForward /tmp/open-forward-%r.sock /tmp/open-forward.sock
                      │                         │
                      │                         └── local socket (on A)
                      └── remote socket (on B), %r = test-user

So when you run: ssh test-user@machineB

MACHINE A                                MACHINE B
─────────                                ─────────
systemd runs socat listening on:         SSH creates socket at:
/tmp/open-forward.sock                   /tmp/open-forward-test-user.sock
        ▲                                        │
        │         SSH TUNNEL                     │
        └────────────────────────────────────────┘

On Machine B, xdg-open wrapper checks:
SOCK="/tmp/open-forward-${USER}.sock"
     = /tmp/open-forward-test-user.sock  ← matches!

Why the username matters (multi-user scenario)

Machine B has two users SSHing in simultaneously:

User 1: alice@machineA  →  ssh test-user@machineB
User 2: bob@machineA    →  ssh admin@machineB

Without username suffix (both use /tmp/open-forward.sock):
  - alice connects first, creates socket
  - bob connects, StreamLocalBindUnlink DELETES alice's socket
  - alice's forwarding breaks!

With username suffix:
  - alice → /tmp/open-forward-test-user.sock
  - bob   → /tmp/open-forward-admin.sock
  - both work independently

socat basics

socat creates a bidirectional channel between two "addresses". Syntax: socat ADDRESS1 ADDRESS2

Common address types:

  • UNIX-LISTEN:/path - Create a listening Unix socket at path
  • UNIX-CONNECT:/path - Connect to existing Unix socket
  • - - stdin/stdout
  • EXEC:"command" - Execute command, pipe data to it

Testing locally (no SSH needed)

Terminal 1 - start listener:

socat UNIX-LISTEN:/tmp/test-open.sock,fork EXEC:"xargs xdg-open"

Terminal 2 - send URL:

echo "https://example.com" | socat - UNIX-CONNECT:/tmp/test-open.sock
# Browser should open!

Inspecting sockets

ss -xl | grep open-forward  # -x for unix, -l for listening
ls -la /tmp/open-forward.sock  # it's just a file (type 's' for socket)

Architecture diagram

[LOCAL MACHINE]                              [REMOTE MACHINE]

1. systemd starts socat                      3. something calls xdg-open URL
        │                                            │
        ▼                                            ▼
   socat listening on                        4. ~/.local/bin/xdg-open (wrapper)
   /tmp/open-forward.sock                           │
        ▲                                           ▼
        │                                    5. socket exists? yes
        │                                           │
        │         SSH TUNNEL                        ▼
        │    ◄─────────────────────────     6. socat sends URL to socket
        │    RemoteForward                   /tmp/open-forward-$USER.sock
        │
        ▼
7. socat receives URL
        │
        ▼
   EXEC:"xargs xdg-open"
        │
        ▼
8. LOCAL browser opens!

Fixing DNS for work VPN

DNS servers werent being set by the VPN (which they should be) - claudes summary:

Problem

VPN connected successfully but all internet connectivity lost:

nmcli c up work
# Connection successfully activated...
ping google.com  # hangs
ping 8.8.8.8     # works fine

Root cause: Split responsibility between routing and DNS resolution.

The VPN correctly became the default route for IP traffic:

default via 10.200.8.1 dev tun0 proto static metric 50

But resolvectl status showed tun0 had no DNS servers:

Link 9 (tun0)
    Current Scopes: LLMNR/IPv4 LLMNR/IPv6
    Protocols: -DefaultRoute        # <-- not handling DNS
    Default Route: no               # <-- DNS queries ignored

DNS queries still went to Virgin Media (194.168.4.100) via wlp98s0, but those packets got routed through the VPN tunnel where Virgin's DNS was unreachable.

┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│ ping 8.8.8.8│ ──── │   tun0      │ ──── │  ✓ Works    │
│ (direct IP) │      │ (VPN route) │      │             │
└─────────────┘      └─────────────┘      └─────────────┘

┌─────────────┐      ┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│ping google  │ ──── │ DNS query to│ ──── │ routed via  │ ──── │ ✗ Timeout   │
│             │      │ 194.168.4.100│     │ tun0        │      │ unreachable │
└─────────────┘      └─────────────┘      └─────────────┘      └─────────────┘

Fix

Assign DNS servers to the VPN connection:

nmcli c modify work ipv4.dns "8.8.8.8 1.1.1.1"
nmcli c down work && nmcli c up work

Result

Link 10 (tun0)
    Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6
    Protocols: +DefaultRoute        # <-- now handling DNS
    Current DNS Server: 8.8.8.8
    DNS Servers: 8.8.8.8 1.1.1.1
    Default Route: yes              # <-- DNS queries go here

Link 2 (wlp98s0)
    Default Route: no               # <-- demoted, no longer used for DNS

Now DNS queries route through tun0 to reachable public DNS servers.

Why OpenVPN didn't push DNS

The work OpenVPN server isn't configured to push DNS settings (push "dhcp-option DNS x.x.x.x"). Client-side config works around this.

Nixos @ symbol

Can use the @ symbol to reference object args (kind of like args: {x: number, y: number} in js)


nix-repl> addWithAlias = args@{ x, y, ... }: x + y + args.z

nix-repl> addWithAlias
«lambda @ «string»:1:2»

nix-repl> addWithAlias { x = 5; y = 10; z = 2; }
17

nix-repl>

nix-repl>

Vimscript functions

So weird weird details of how functions work in vim

  • system() is a FUNCTION
  • :echo is a COMMAND

and functions CANNOT be called directly from the command line!

Hence why :system(...) does not work! need :echo or :call to invoke it

Even weirder... even inside a vimscript function this still applies? which seems strange to me but oh well... So like doing : to invoke a command the same applies in vimscript in will complain if you try and do

function! OpenMarkdownPreview(url)
  system('echo hi')
end

will yield

E492: Not an editor command: system('echo hi')

as you need echo or call!

DBus (Interacting with system services)

DBus is a message bus system that allows applications to talk to one another. You can interact with it using busctl.

1. List available services on the user bus

busctl --user list | head -n 5
# NAME                                PID PROCESS
# :1.0                               2111 systemd
# :1.1                               2124 .agent-wrapped
# org.freedesktop.Notifications      2345 dunst

2. Introspect a service (see methods/properties)

busctl --user introspect org.freedesktop.Notifications /org/freedesktop/Notifications
# NAME                          TYPE      SIGNATURE RESULT/VALUE
# .Notify                       method    susssasa{sv}i u
# .Dnd                          property  b         false

3. Call a method (Send a notification)

# Note: Single quotes are CRITICAL for ZSH users (signature AND strings with '!')
busctl --user call org.freedesktop.Notifications /org/freedesktop/Notifications \
  org.freedesktop.Notifications Notify 'susssasa{sv}i' \
  "Gemini-CLI" 0 "dialog-information" "Hello" 'Sent via DBus!' 0 0 5000
# u 1 (returns the notification ID)

4. Get a property

busctl --user get-property org.freedesktop.Notifications /org/freedesktop/Notifications \
  org.freedesktop.Notifications Dnd
# b false

DBus Concepts & Troubleshooting

1. ZSH Issue: Quotes & Expansion

  • The Signature: {sv} is interpreted as brace expansion. You must use single quotes: 'susssasa{sv}i'.
  • The Message: ! inside double quotes triggers history expansion in interactive ZSH. Use single quotes: 'Sent via DBus!'.
# BAD (ZSH tries to expand {sv} and !)
busctl ... Notify susssasa{sv}i ... "Hello!"

# GOOD
busctl ... Notify 'susssasa{sv}i' ... 'Hello!'

2. The Hierarchy: Service vs Object vs Interface It helps to think of it like an Object-Oriented generic URL: Service -> Object -> Interface -> Method

TermAnalogyExample
ServiceThe Domain Name (DNS)org.freedesktop.Notifications
Object PathThe File Path (URL path)/org/freedesktop/Notifications
InterfaceThe Class Type (Java/C#)org.freedesktop.Notifications
MethodThe FunctionNotify(...)

3. "Are you guessing the path?" No. While they often match the service name (replacing . with /), they don't have to. You find them using tree:

busctl --user tree org.freedesktop.Notifications
# └─ /org
#   └─ /org/freedesktop
#     └─ /org/freedesktop/Notifications  <-- The Object Path

4. "Is that a socket path?" No. It looks like a file path, but it is a logical name used purely within DBus to organize objects inside a service. It does not exist on your hard drive.

Pretty cool i can see the notify method in dbus directly here!

➜ jollof dev-setup (main) ✗ busctl --user introspect org.freedesktop.Notifications /org/freedesktop/Notifications | grep -i notify
.Notify                             method    susssasa{sv}i u            -
➜ jollof dev-setup (main) ✗

Systemd Learning

1. Visibility of units

When running systemctl --user status, it defaults to showing the active service tree (specifically default.target).

  • Sockets often sit quietly in the background waiting for a connection. Unless they are actively processing something or triggered an error, they might not show up in the main status tree.
  • To see sockets specifically: systemctl --user list-sockets (this shows what ports/files systemd is listening on).
  • To see everything systemd knows about (loaded units): systemctl --user list-units

2. The @ symbol (Template Units)

The @ symbol denotes a Template Unit.

  • Normal Unit: my-service.service -> One config, one instance.
  • Template Unit: my-socket@.service -> One config, many possible instances.

When you have Accept=yes in a socket file (my-socket.socket), systemd does this magic:

  1. It listens on port 8888.
  2. Connection comes in from IP 1.2.3.4.
  3. Systemd looks for a service named my-socket@.service.
  4. It instantiates it as my-socket@0-127.0.0.1:8888-1.2.3.4:5678.service (or similar unique ID).
  5. It runs that specific instance for that specific connection.

This is how SSH works (sshd@.service) handling multiple users at once!

3. What is a "Unit"?

A Unit is just the generic object name for "thing systemd manages". Everything in systemd is a unit. The suffix tells you what type of unit it is:

  • .service: A process/script (The Worker)
  • .socket: A network port/file (The Phone Line)
  • .timer: A schedule (The Alarm Clock)
  • .target: A group of other units (like "Multi-User Mode" or "Graphical Mode")
  • .mount: A filesystem mount point

They link together via dependencies:

  • Timer Wants -> Service (starts it when time is up)
  • Socket Activates -> Service (starts it when call comes in)
  • Target Wants -> Services (starts bunch of them at boot)

Jollof notes

Ok (above is gemini)

So if i enabled the my-socket.socket and then I've opened 2 zshs with this

➜ jollof dev-setup (main) ✗ nc localhost 8888
Hello from Systemd Socket! Type something and press Enter:
asdf
asdf

then I can see below

➜ jollof dev-setup (main) ✗ systemctl --user list-sockets
LISTEN                                        UNIT                     ACTIVATES
[::]:8888                                     my-socket.socket         my-socket@2-12289-::1:8888-::1:36326.service
                                                                       my-socket@1-8193-::1:8888-::1:60746.service
/run/user/1000/bus                            dbus.socket              dbus.service
/run/user/1000/gnupg/S.gpg-agent              gpg-agent.socket         gpg-agent.service
/run/user/1000/pipewire-0                     pipewire.socket          pipewire.service
/run/user/1000/pipewire-0-manager             pipewire.socket          pipewire.service
/run/user/1000/pulse/native                   pipewire-pulse.socket    pipewire-pulse.service
/run/user/1000/speech-dispatcher/speechd.sock speech-dispatcher.socket speech-dispatcher.service

7 sockets listed.
Pass --all to see loaded but inactive sockets, too.
➜ jollof dev-setup (main) ✗

My socket has 2 connections! It's activated the template and I can see my-socket@1.... and my-socket@2

  • Implicit by Name (Magic Naming): By default, my-name.socket activates my-name.service (or my-name@.service if Accept=yes).
  • Explicit Override: Use Service=other-service.service in the [Socket] section to break the naming convention and trigger a different unit.

Accept=yes vs Accept=no

  • Accept=yes: Systemd accepts the connection itself and spawns a new instance of the template service (@.service) for each incoming connection. This is how SSH handles multiple simultaneous users.
  • Accept=no (Default): Systemd passes the listening socket to a single service instance. The service is responsible for calling accept() (like a web server or database).

Systemd Hierarchy: Slices, Scopes, and Services

The Concept: Think of it like a file system for processes.

  • Cgroups (Control Groups): The actual directories in the kernel that track processes.
  • Slices (.slice): The "folders". They organize things hierarchically and control resources (e.g., "System gets 80% CPU, User gets 20%").
  • Services (.service) & Scopes (.scope): The "files". These contain the actual running processes.

The Tree:

  1. Root (-.slice): The top of the tree.
  2. user.slice: Holds all user sessions.
  3. user-1000.slice: Your specific user (UID 1000).
  4. user@1000.service: The systemd manager instance for you.
    • app.slice: Applications started by the GUI or systemd (like firefox, my-socket).
    • session.slice: Core session stuff (like dbus, pipewire).

Interactive Commands:

  1. Visualize the Tree: systemd-cgls Shows the entire hierarchy recursively.

  2. Monitor Resources (The "Top" for Cgroups): systemd-cgtop Shows CPU/Memory usage per Slice/Service.

  3. Inspect Your User Tree: systemctl --user status Shows what your specific systemd instance is managing right now.

  4. Creating a Temporary Scope (Fun Test): Run a command in a specific slice to limit its resources. systemd-run --user --scope -p MemoryMax=10M --unit=my-test bash (This starts a bash shell that systemd kills if it uses >10MB RAM).

Treesitter

Why no matter how many times do i google this i forget it ffs - TOGGLING TREESITTER TREEEEEEE......

:InspectTree

Qmk compiling

I haven't compiled my QMK keyboard in an age so here's the command again

I think i will keep space as special key on my keychron as right alt is a bit hard to access under right thumb. But I will move keymaps to matching ./modules/nixos-keyd.nix

  1. To setup
gh repo clone joeledwardson/qmk_firmware ~/qmk_firmware
cd ~/qmk_firmware
qmk setup
  1. Finding my keyboard
➜ joelyboy qmk_firmware (master) ✗ qmk list-keyboards | grep -i q4
keychron/q4/ansi/v1
keychron/q4/ansi/v2
keychron/q4/iso
p3d/q4z
➜ joelyboy qmk_firmware (master) ✗
  1. compiling: qmk compile -kb keychron/q4/iso -km joelyboy

  2. unplug keyboard and re-plug in pressing the flash button underneath the space bar

Will need another keyboard at this stage

  1. then qmk compile -kb keychron/q4/iso -km joelyboy