errenil https://linerre.github.io/ General description of the blog Sun, 25 Jan 2026 09:11:13 GMT en Lume 3.1.4 Interacting with blockchains https://linerre.github.io/2026/01/17/interact-with-blockchain.html https://linerre.github.io/2026/01/17/interact-with-blockchain.html Shame on me

My very first programming job was to develop dApps at Race Protocol. It is a very small team building a secure protocol for on-chain games (aka GameFi). While coding there for 2+ years, I have never got down to interacting with various blockchains. I've written smart contracts for both Solana and Sui; I've earned a small amount of cryptocurrency on Solana and known essetial concepts like wallet, address, token, mint, gas, and so on; I've integrated wallet (phantom) into a dApp designed for users to mint their NFTs; I've coded in Rust a merkle tree SDK for uploading files to Arweave on command line. However, I'm not familiar with reading data from various chains via (usually) JSON RPC endpoints, which is what this blog is mainly about.

Scan & Explorer

You don't even need a wallet (or on-chain address/account) to start reading data (transactions, blocks, validators, etc) from a specific blockchain. The easiest way to do so immediately is use a corresponding scan or explorer, which is basically a web interface for viewing blockchain data within your browser: Solscan or Explorer | Solana for Solana, Ethereum (ETH) Blochain Explorer (aka Etherscan) for Ethereum, BscScan or BSC Trace for BNB Smart Chain, just to name a few. Whenever you want to inspect the data on a certain chain, you can directly use such a tool (if you know one already) or simply search for one with "XXX explorer", where "XXX" is the chain name.

The above explorers, though quite useful most of the time, cannot fit in all use cases. For example, if you want to know, in a given time range, which transaction has the lowest gas or transaction fee. It is doable using a scan but it is also tedious and error prone. Besides, as a programmer, you might already begin wondering: where do these explorers get the raw/source data for display? This brings us to the next topic.

Library, framework, SDK, API Key, endpoints

As you dive deeper into blockchain ecosystems, you are likely to access chain data programmatically. To do so, you'll need at least:

  1. A private or public1 endpoint, which is basically a URL that accepts HTTP(S) requests with JSON data
  2. A library or SDK2 that has already implemented interface to communicate with blockchains

A private endpoint is usually provided by a RPC node provider such as Alchemy, Quicknode, Infura and so. Alchemy has a list of 43 RCP Node Provders for 2025. I'm using Infura for no particular reasons. Later on you'll agree that, for general use, they share more in common than they differ. Infura's free plan is more than enough for the intention of this blog. After you create your first Infura API key, you are ready to go.

As to the library/SDK, it is equally confusing because of too many choices. Basically, over the past few years, the most popular option is the now legacy (but still being used by many) web3.js. Its Python counterpart is web3.py. In particular, Solana has solana/web3.js to that end. Now in 2026, as far as I know, the recommended options are as follows

  1. For interacting with EVM chains, choose ethers.js, be it v5 or v6
  2. Viem is yet another option for Ethereum
  3. For interacting with Solana, choose solana/kit

Since I prefer Typescript and Rust, I won't cover any Python library in this blog.

Read data from EVM chains on command line

Let's take BNB Smart Chain for example. At the time of writing this line, the most recent block 75757661. Open the block in BscScan, we can see it included 64 transactions, among other info. Now let's try to fetch the block info using command line

curl https://bsc-mainnet.infura.io/v3/<YOUR-API-KEY> \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": ["0x483F85D", false], "id": 1}'

Running the command and I got the below output (some folded for better display):

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "baseFeePerGas": "0x0",
    "blobGasUsed": "0x0",
    "difficulty": "0x2",
    "excessBlobGas": "0x0",
    "extraData": "0xd2830106...cfb177674b401",
    "gasLimit": "0x3473bc0",
    "gasUsed": "0xacf31e",
    "hash": "0xdc789f351e0de1de23af83748b445079b3c56ce723d6b9b565c982523c7a0a82",
    "logsBloom": "0x482442e1...0e11c08e3112623",
    "milliTimestamp": "0x19bcae69012",
    "miner": "0x7e1fdf03eb3ac35bf0256694d7fbe6b6d7b3e0c8",
    "mixHash": "0x00000000000000000000000000000000000000000000000000000000000001c2",
    "nonce": "0x0000000000000000",
    "number": "0x483f85d",
    "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "parentHash": "0x51096081a24a3876dec7e757d36ffab17fc0e08f95d49137cb62bd95303cf29b",
    "receiptsRoot": "0x65ce2d285a81db09fd1684bb5a0d1365020b6b6b053186e3222bb894c37993d7",
    "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
    "size": "0xbe78",
    "stateRoot": "0xceb2aab6f5d6a4c5dd0567f45d70ce1fdbc2d440c2287722b6809a439aa00610",
    "timestamp": "0x696b3d12",
    "totalDifficulty": "0x8ff0b02",
    "transactions": [
      "0x2cb349e6e0040d05a6cf5413585de3af463d77a8f474ad802c7e2244d4952dee",
      "0xa7f324e1323acbe532774d23f226e6e31f0c119b1f2d94039af4fa1e0ef34b99",
      "0xa580a8f9177a6e5fb5ead85efdfc4d0c1c2b4967ddc50154353e07a05b68180e",
      ......
      "0xf293628839c99ef8fbcb9519e9cb306e6422b1720fb5063caf70560cb47fc973",
      "0xf72f99b97aedaf0381cd2143a7c436acef8b40e3f4251c37f94c4260a536f7c8",
      "0x32c2f26cd493654532d2013b2c57b1940a0708f7213e9d0b4653921ab2b5b9b3"
    ],
    "transactionsRoot": "0x14e09a42ed5469f1e4762710af3c764aab6dc11b4895554911cf09937fe9a08c",
    "uncles": [],
    "withdrawals": [],
    "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
  }
}

To get more information about method eth_getBlockByNumber, read the doc for it or Infura's doc . For all available methods, see the JSON API Reference of Ethereum. You may want to try out methods like eth_getTransactionByHash or eth_getTransactionCount (for fetching nonce). Note, however, each such call will consume your daily credit allocated to your API key. Infura has a general review for different plans. As to this particular method, eht_getBlockNumber, it costs 80 credits. 3

Since all these RPC node providers are indeed compatible with Ethereum JSONRPC API, you can expect they have similar API docs. For example, NodeReal lists how many compute units4 a method costs when using its private endpoint. Chiannodes's doc shows what methods it supports, with explanation, for which (EVM-based) chain.

Read data from EVM chains using ethers.js

The latest release of ethers.js is v6.16.0. Yet for this blog I'm using v5.8.0 installed via the below command:

$ npm install -D ethers@5

ethers.js v6 has a considerable number of breaking changes. If you do want to use v6, make sure you are referencing the correct document. Interesting enough, at the time of writing this blog, Google search lists the document for ethers v5 as the top result, even though it is marked with v6:

Ethers v6 link directs to the v5 document

Using ethers.js to interact with EVM chains is pretty much like using cURL. The below function fetches the information for the given transaction hash on Ethereum (if chain is undefined) or on the passed chain.

export async function findTx(txHash: string, chain?: string) {
  try {
    // Decide chain and default to ethereum
    const _chain = chain ?? 'ethereum';
    const _chainId = CHAINS_TO_IDS[_chain];
    const endpoint = `${INFURA_ENDPOINTS[_chain]}${process.env.INFURA_API}`;
    console.log('Finding transaction on the following chain:');
    console.group();
    console.log(`Chain: ${_chain} with ID: ${_chainId}`);
    console.log(`Provider URL: ${endpoint}`);
    console.log('Tx hash:', txHash);
    console.groupEnd();

    const provider = new ethers.providers.JsonRpcProvider(
      endpoint,
      {name: _chain, chainId: _chainId}
    );

    const tx = await provider.getTransactionReceipt(txHash);

    if (!tx) {
      const _tx = await provider.getTransaction(txHash);
      console.log('-- Result of getTransaction --');
      console.group();
      console.log(JSON.stringify(_tx, null, 2));
      console.groupEnd();
    } else {
      const block = await provider.getBlock(tx.blockNumber);
      const utc = new Date(block.timestamp * 1000).toISOString();
      console.log('-- Result of getTransactionReceipt --');
      console.group();
      console.log(JSON.stringify(tx, null, 2));
      console.log('Transaction Timestamp (UTC)', utc);
      console.groupEnd();
    }

    process.exit(0);
  } catch (error) {
    console.error("Error fetching data:", error);
    process.exit(1);
  }
}

The CHAINS_TO_IDS and INFURA_ENDPOINTS are two constants defined elsewhere for convenience. For EVM chains and their ids, visit ChianList. Note that all transactions in the same block have the block's timestamp.

Read data from Solana on command line

Solana is rather different from EVM chains and has its own command line tool. But for reading data from Solana, you can still start with simple curl command.

Let's try to do the same: get the latest block info. At the time of writing this line, the block height is 372240007. Since Infura focuses mainly on EVM-compatible chains, we need yet another provider. Again, for no particular reasons, I choose Alchemy.

curl https://solana-mainnet.g.alchemy.com/v2/<YOUR-API-KEY> \
     --request POST \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '{"id":1,"jsonrpc":"2.0","method":"getBlock","params":[372240007, {
         "commitment": "finalized",
         "encoding": "json",
         "transactionDetails": "full",
         "maxSupportedTransactionVersion": 0,
         "rewards": false
       }
   ]}'

The output (folded for better display) is as follows

{
  "jsonrpc": "2.0",
  "result": {
    "blockHeight": 350408185,
    "blockTime": 1760018519,
    "blockhash": "CweRBCmzcQRq9cs93XFiFuGzKrsrM8o8tJ298xmXifiD",
    "parentSlot": 372240006,
    "previousBlockhash": "gBPmktg3nQqMdbc7qxfbWRGsTs24M4pagBwXykFgwHi",
    "transactions": [ ... ]
    .....
    }
}

For detailed info about this method, visit its document page. Also you can try out each method interactively on the very document page for that method. Similarly, Alchemy also lists the compute units for all the methods it supports.

Read data from Solana using Solana Kit

Solana sunsets web3.js in favor of Solana kit. The latter is more modular and lightweight. Solana also shows complete code using kit to get a block's info:

import { createSolanaRpc } from "@solana/kit";

const rpc_url = `https://solana-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`;
const rpc = createSolanaRpc(rpc_url);

const slot_number = BigInt(372240007);

let block = await rpc
  .getBlock(
    slot_number,
    {
      commitment: "finalized",
      encoding: "json",
      transactionDetails: "full",
      maxSupportedTransactionVersion: 0,
      rewards: false,
    },
  )
  .send();

console.log("block:", block);

Once you know how to read data from chains, you may want to begin sending transactions. To do so, besides an endpoint and a SDK, you will also need a wallet address. For EVM chains, the most popular choice is Metamask; for Solana, it is likely Phantom.

It may be difficult to get some initial funds on the mainnets for these chains. But you can start with the devnets. In addition, this may be a good time to get into the world of DeFi. Like there are a great number of chains already, there are many CEXs and DEXs too. All these may serve as the topic for a future post.

]]>
Sat, 17 Jan 2026 00:00:00 GMT
Linked Lists in Clojure https://linerre.github.io/2025/09/18/linked-list-in-clojure-breif.html https://linerre.github.io/2025/09/18/linked-list-in-clojure-breif.html In Clojure, lists are implemented as linked lists by default. Considering its immutable data structures, after reverse-ing a list, we get a completely new list in memory:

(let [original '(1 2 3 4 5)
      reversed (reverse original)]
  (println "Original:" original)
  (println "Reversed:" reversed)
  (println "Same object?" (identical? original reversed))
  (println "Original address:" (System/identityHashCode original))
  (println "Reversed address:" (System/identityHashCode reversed)))

The above code, if run in the REPL, gives the below result on my machine:

Original: (1 2 3 4 5)
Reversed: (5 4 3 2 1)
Same object? false
Original address: 1680485275
Reversed address: 583859661

If the result is what we care about only, this is great. But if we want to manipulate the same nodes of a linked list in the memory, as far as my level of Clojure can go, it seems impossible1. This also makes it fairly difficult (if not impossible) for Leetcode to support Clojure when it comes to linked list problems. Indeed, any problems that expect mutating in-memory data in place will likely cause troubles. Besides, Leetcode sometimes explicitly prohibits this, as Leetcode 25 mentions:

You may not alter the values in the list's nodes, only nodes themselves may be changed.

If a problem doesn't even allow programmers to alter the node values, it will definitely throw errors at any attempt to create a linked list with entirely new nodes.

The rest of this post will nevertheless try to solve 3 Leetcode problems about linked lists, with a simple focus: to get a linked list where the order of nodes is as expected.

Data structure

To better imitate the most common linked lists, we need to define a similar data structure for the nodes of a singly linked list:

(deftype ListNode [value next])

Thus, to make a linked list out of thin air:

(defn make-linked-list
  "Create a head-only singly linked list.  Return the head."
  [value]
  (ListNode. value nil))

We also need to be able to insert a new node at the list head (a.k.a prepend):

;; O(1)
(defn prepend-node [current value]
  (ListNode. value current))

After inserting some nodes, we will want to know the length of the list. Thus:

(defn count-nodes [head]
  (loop [cnt 1
         cur head]
    (if (nil? (.next cur))
      cnt
      (recur (inc cnt) (.next cur)))))

As a dumb human being, I often believe what I see, so God please turn on the light:

(defn print-list
  [head]
  (if (nil? head)
    (println "Empty list")
    (loop [cur  head
           vals [(.value cur)]]
      (if (nil? (.next cur))
        (println (string/join " -> " vals))
        (recur (.next cur) (conj vals (-> cur .next .value)))))))

Now fire up the REPL and try out the below:

(-> (ListNode. 1 (ListNode. 2 (ListNode. 3 (ListNode. 4 (ListNode. 5 nil)))))
    (print-list))
;;=> 1 -> 2 -> 3 -> 4 -> 5

Note, all the above helpers take a ListNode as their first argument.

Leetcode 206: Reverse Linked List

Mutation is forbidden and immutability results in new. Isn't it cool? Instead of node1.next = nil, we prepend node1 to nil, then node2 to the chain of node1 -> nil, and so on:

(defn reverse-list [head]
  (loop [prev nil
         curn head]
    (if (nil? curn)
      prev
      (recur (prepend-node prev (.value curn)) (.next curn)))))

In the REPL, try

(comment
  (-> (ListNode. 1 (ListNode. 2 (ListNode. 3 (ListNode. 4 (ListNode. 5 nil)))))
      (reverse-list)
      (print-list)))
;;=> 5 -> 4 -> 3 -> 2 -> 1

Simple, trivial, isn't it?

Leetcode 92: Reverse List II

This is one of the most awkward things I did with Clojure. I tried hard to not introduce any extra vectors or lists to hold the intermediate list nodes, at the expense of looping over the list one more time.

The first task we need to complete is find the node that lies immediately before the starting left2 node. This is because, the problem statement asks us to reverse only the nodes from position left to right (inclusive and left <= right). If we start with the exact node at position left, we will lose the connection with all the previous nodes.

The tricky part for a singly linked list is (almost3) always its head, as it has no previous node. A common workaround is to create a dummy node as the head. Thus, when left = 1, we start with the real (original) head node of the list. Also, when working with a linked list, we can't be too careful due to the possible empty lists (i.e. when head is nil, null, None, etc):

(defn find-prev
  "Position `postion` (1-indexed) is guaranteed to be <= list length.
  This is equivalent to return the previous node of `positoin`.
  If the `position` is 1 (head), return the dummy node."
  [dummy position]
  (if (nil? dummy)
    nil
    (loop [curn dummy
           curp 0]
      (if (= curp (dec position))
        curn
        (recur (.next curn) (inc curp))))))

It just feels criminal if we have find-prev without the corresponding find-next. Agree with me? Hopefully. For a singly linked list, its tail has a perfect mark: no next node

(defn find-next
  "`position` is 1-indexed is guaranteed to be <= list length.
  No need for dummy in this search"
  [head position]
  (if (nil? head)
    nil
    (loop [curn head
           curp 1]
      (if (= curp position)
        (.next curn)
        (recur (.next curn) (inc curp))))))

Wait, wait, our toolkit still misses one important widget to find the node at the given position:

(defn find-node
  "`position` (1-indexed) is gunranteeded to be <= list length."
  [head position]
  (if (nil? head)
    nil
    (loop [curn head
           curp 1]
      (if (= curp position)
        curn
        (recur (.next curn) (inc curp))))))

Now that we've got the tools ready, let's get down to work. A few key points:

  1. order of nodes before position left must be retained
  2. order of nodes after position right must be retained
  3. due to the immutability, we have to prepend the nodes in point 1 to the reversed part one by one (done by the inner loop, which runs once, so totally still O(n))
(defn reverse-between
  "Reverse the part between `left` and `right` (1 <= left <= right).
  Return the reversed list."
  [head left right]
  (let [dummy    (prepend-node head "dummy")
        pn       (find-prev dummy left)
        nr       (find-next head right)]
    (loop [prev nr                 ; start prepending onto to the rest
           curn (.next pn)
           cnt  0]
      (if (= cnt (+ 1 (- right left)))
        (loop [oprev pn
               ret prev
               pos (dec left)]
          (if (= pos 0)
            ret
            (recur (find-prev dummy pos)
                   (prepend-node ret (.value oprev))
                   (dec pos))))
        (recur (prepend-node prev (.value curn))
               (.next curn)
               (inc cnt))))))

Note, the prepending is done in reversed order in order to retain the original order. I admit this is not a good solution but it is what I can come up with for now.

Leetcode 25: Reverse Nodes in k-Group

While doing the above Problem 92, it occurred to me that I could not only have prepend-node, but also prepend-list that inserts an entire linked list before the head of current list:

(defn prepend-list [node lst]
  (loop [n (count-nodes lst)
         head node
         curn (reverse-list lst)]
    (if (zero? n)
      head
      (recur (dec n)
             (prepend-node head (.value curn))
             (.next curn)))))

It is like we push the nodes onto a stack and then pop them one by one when prepending. And this stack-like approach is the key to the solution for the current problem.

Now, for each k nodes, we need to reverse them. With the stack-like behavior in the mind, if we take while building a new list (in Clojure we just have to), isn't it sweet that we will just get a reversed list of the taken-out nodes?

(defn take-n-nodes-reverse
  "Take n nodes starting with head. The resulted linked list is a perfectly
  reversed one. If n is larger than the list length, equivalent to reversing
  the entire list."
  [head n]
  (if (nil? head)
    head
    (loop [lst nil
           curn head
           cnt 1]
      (if (or (nil? (.next curn)) (= cnt n))
        (prepend-node lst (.value curn))
      (recur (prepend-node lst (.value curn))
             (.next curn)
             (inc cnt))))))

take-n-nodes-reverse

But what if we do need to take n nodes and retain their original order? Push-pop again!

(defn take-n-nodes [head n]
  (if (nil? head)
    nil
    (-> head
      (take-n-nodes-reverse n)
      (reverse-list))))

With these helpers, let's break the problem into the following steps:

  1. find out the number of k-node-groups (list-len / k)
  2. find out if there are any remaining nodes (list-len % k)
  3. use take-n-nodes-reverse to reverse the k-node groups
  4. prepend the reversed groups to the remaining nodes

Sounds good? Actually, the reality is ugly: this is a singly linked list and there is no easy way for quick access to the remaining (tail) nodes. It is however trivial to access the nodes from and/or near the head. Aha, let's reverse the list at the beginning and we will get the k-node groups already reversed. A bonus!

The process looks roughly like below:

reverse-k-node-groups

(defn reverse-k-group
  [head k]
  (let [l     (count-nodes head)
        m     (rem l k)                    ; number of out-of-k-group nodes
        rhead (reverse-list head)]
    (loop [rcurn  (if (zero? m) rhead (find-next rhead m))
           group  (take-n-nodes rcurn k)
           cnt    0
           mnodes (if (zero? m) nil (take-n-nodes-reverse rhead m))]
      (if (or (nil? rcurn) (= l (+ m cnt)))
        mnodes
        (recur (find-next rcurn k)
               (take-n-nodes (find-next rcurn k) k)
               (+ k cnt)
               (prepend-list mnodes group))))))

The second condition (= l (+ m cnt)) might be redundant but I'm just too lazy to care about it in this case.

Even though it's fun to explore how Clojure handles linked lists, I'd say it also feels a bit awkward. While I'll continue to solve LeetCode-style problems with Clojure from time to time, I doubt I'll delve deeper into linked lists using the language.

]]>
Thu, 18 Sep 2025 00:00:00 GMT
Remap CapsLock to Escape in Linux TTY https://linerre.github.io/2024/10/28/remap-capslock-in-tty.html https://linerre.github.io/2024/10/28/remap-capslock-in-tty.html These days I tend to ssh to my Gentoo machine more and more, so most of the time I just stay in a tty environment as root user (Yep, I like managing my packages as root user directly). The only problem is I'd like to remap the CapsLock to Escape for better editing experience in Emacs with Meow.

TL;DR

Jump to the last section if you are only curious about what I ended up with.

My first attempt

The first thing that occurred to me was: let me ask AI (in this case, Claude) to quickly walk me through mapping CapsLock to Escape in tty:

When in a tty (Linux), how can I map Caps lock to escape?

Claude's response:

For a TTY (virtual console) in Linux, you can use the loadkeys command with a custom keymap. Here's how to do it:

Create a temporary keymap file

$ cat > /tmp/caps2esc.map << 'EOF'
keymaps 0-127
keycode 58 = Escape
EOF

Load the keymap

$ loadkeys /tmp/caps2esc.map

Optional: To make it permanent, copy to a persistent location

$ sudo cp /tmp/caps2esc.map /usr/local/share/kbd/keymaps/
$ sudo loadkeys /usr/local/share/kbd/keymaps/caps2esc.map

Add to /etc/rc.local for persistence across reboots:

$ sudo loadkeys /usr/local/share/kbd/keymaps/caps2esc.map

This will immediately remap your Caps Lock key to Escape. Note that:

  1. This only works in TTY/virtual console, not in X11 or Wayland
  2. You'll need root privileges to run loadkeys
  3. The changes will be reset after a reboot unless you make them permanent using the commented options

Would you like me to explain any part of this in more detail?

Reflection v1

I tried it out in a slightly different way:

  1. as the root user, create the ~/caps2esc.map file with the suggested content
  2. loadkyes immediately after creating the above file

It did work but if I want to make it persistent, I need to follow the instruction in the above 2nd option. The thing that stopped me from doing so was that I didn't find any directory named kbd/keymaps under /usr/local/share. Clearly, Claude assumed that. Since I didn't tell it which Linux distro I'm using, it's fine.

My second attempt

Since the temporary approach worked and I didn't really create the needed directory structure just for this simple task, I was thinking the following:

As a root user, can I add the following line to my .bash_profile ?

Claude response:

No, I wouldn't recommend adding that line to .bash_profile for a couple of reasons:

The /tmp directory is cleared on reboot, so your keymap file won't persist loadkeys affects the entire system (all TTYs/virtual consoles), not just your session Running loadkeys from .bashprofile would try to remap keys every time you log in or open a new bash session, which could cause errors or unexpected behavior

Instead, if you want to make this permanent, I recommend either:

Store the keymap in a persistent location:

$ cp /tmp/caps2esc.map /usr/local/share/kbd/keymaps/

Then add the loadkeys command to /etc/rc.local (which runs once at boot time):

$ loadkeys /usr/local/share/kbd/keymaps/caps2esc.map

Or use systemd by creating a one-time service that runs at boot.

Would you like me to show you how to set up either of these permanent solutions?

Reflection v2

Again, I didn't want to create the /usr/share/local/kdb/keymaps/ directory just for this simple task and I did wonder why Claude kept suggesting this. Perhaps it didn't know I'm using Gentoo Linux?

My third attempt

This time, I let it know I want to do this on a Gentoo distro:

I'm using Gnetoo Linux with OpenRC, the Wiki suggests /etc/conf.d/keymaps. What would you suggest?

Claude response

Ah yes, for Gentoo with OpenRC, using /etc/conf.d/keymaps is indeed the proper way! This is much better than the solutions I suggested earlier. You have two good options:

If you want to keep your custom keymap separate:

Create the keymap in a persistent location

$ mkdir -p /usr/local/share/keymaps
$ cp /tmp/caps2esc.map /usr/local/share/keymaps/

Edit /etc/conf.d/keymaps:

/etc/conf.d/keymaps

keymap=/usr/local/share/keymaps/caps2esc

Or, simpler approach: just add the remap directive directly to your existing keymap by editing /etc/conf.d/keymaps:

/etc/conf.d/keymaps

keymap="us"  # or whatever your current keymap is
windowkeys="yes"  # if not already set
extended_keymaps="caps2esc"
keymap_custom="keycode 58 = Escape"

The second approach is more "Gentoo-like" as it uses the built-in configuration system. Would you like me to explain the differences between these approaches in more detail?

Reflection v3

This time it understands the Gentoo way but I was confused about the keymap_custom variable in Option 2 because, according to the Gentoo Wiki: Keyboard layout switching, there are only keymap and extended_keymaps. Also, Option 2 uses "caps2esc" directly without even mentioning where this keymap should be placed. I decided to let Claude check again.

My fourth attempt

for the second approach, how does extended_keymaps="caps2esc" work? I don't see where the config can find the map caps2esc

Claude response

Ah, I apologize for my mistake. You're absolutely right. I included extended_keymaps="caps2esc" incorrectly. This setting is for loading predefined keymaps from /usr/share/keymaps/, and "caps2esc" isn't a standard keymap.

For your case, you only need:

/etc/conf.d/keymaps

# Set your regular keymap
keymap="us"  # or whatever your current keymap is

# Add the caps lock remap
windowkeys="yes"  # if not already set
keymap_custom="keycode 58 = Escape"

The keymap_custom variable is what you need - it allows you to specify direct keycode mappings that will be applied after your base keymap is loaded. I apologize for introducing confusion with the incorrect extended_keymaps setting.

Reflection v4

Good. It seems there's no need to bother with /usr/local/share/keymap, but it still suggests using key_custom variable directly. My intuition was that if this variable is so convenient, why doesn't Gentoo Wiki even slightly hint it? I tried out the suggestion any and it didn't work. I thought as much.

My Final attempt

Is there a keymap_custom variable? Can you double check?

Claude response

I apologize for my confusion - you're right again. I was mistaken about the keymap custom variable. I should have verified this in the Gentoo documentation first. Looking at the Gentoo Wiki and OpenRC documentation, for custom keymap modifications, the proper approach would be:

Either create a complete custom keymap file:

$ mkdir -p /usr/local/share/keymaps

# Create your custom keymap based on your current one
$ dumpkeys > /usr/local/share/keymaps/personal.map

Edit the map to change Caps Lock to Escape: keycode 58 = Escape

/etc/conf.d/keymaps

keymap="/usr/local/share/keymaps/personal"

Or modify the keymap at boot time using a local.d script.

I apologize for my previous incorrect answers. Would you like me to explain either of these approaches in more detail?

Reflection v5

Alright, it seems Claude was at the end of its rope. Surprised? My prompts were arguably bad too, perhaps.

The solution

It turned out that the Wiki already shows the correct steps, just in a confusing order. To remap CapsLock to Escape in Linux tty, follow the below steps:

Use an editor (probably Emacs, vi[m] or nano) to create a <keymap>.map file. <keymap> can be an arbitrary name but it's better to use an informative one, such as caps2esc in this case

In the newly created file, add the following content:

/path/to/caps2esc.map

include "us.map"
keycode  58 = Escape

Compress the file

$ gzip caps2esc.map
$ cp caps2esc.map /usr/share/keymaps//i386/qwerty/

Put this file under /usr/share/keymaps/i386/qwerty/. Note, this is a Gentoo specific path. Consult your distro's manual or wiki for the appropriate path.

In the file /etc/conf.d/keymaps, use the newly created map above

/etc/conf.d/keymaps

keymap="caps2esc.map"

Restart the keymap service. Since I'm using OpenRC:

$ rc-service keymaps restart
$ rc-update add keymaps boot # redundant but harmless

By "confusing order", I mean steps 1-3 are in Section Custom keymap which appears after step 4 in Section OpenRC.

The entire trial-and-error process reminds me of the Queen's Raido Gaga which sings:

You had your time, you had the power. You've yet to have your finest hour. Raido, someone still loves you.

I'd say: Wiki, someone still loves you.

]]>
Mon, 28 Oct 2024 00:00:00 GMT
Good things I sometimes forgot when coding https://linerre.github.io/2023/10/02/forgot.html https://linerre.github.io/2023/10/02/forgot.html After coding for a while, I now find myself, from time to time, caught in situations where I wrote some bad(-style) code because I forgot the good. The reasons vary but this one stands out: I did not really take a concept or definition seriously at the very beginning and did not make it a habit. This post is therefore a note for now and future me.

if statement vs if expression block

In Python's term, if is a statement instead of expression. In Python's if blocks, rarely did I even think about returning a value or result.

In Rust, however, since if is followed by a block, it can also act as expressions, which means it is fairly common in Rust to return a value from a if block:

// Common practice
let x = if a != 10 { a } else { a + 1 };

// Instead of this
let mut x = 0;
if a != 10 {
    x = a;
} else {
    x = a + 1;
}

Or in Haskell, the mandatory else branch requires something to be returned:

doubleSmallNumber x = (if x = 10 then x else x*2) + 1

Or in Clojure:

(if (= a 10)
  a
  (* a 2))

Boolean expressions

My die-hard habit is the following:

if x != 10 {
    correct = false;
} else {
    correct = true;
}

But why not simply this:

correct = x != 10;

In many languages, when an expression evaluates to a value, I should use the expression's return value, rather than merely evaluate it.

.length v.s. len()

It's normal to loop over an array using something like

for (int i=0; i < arr.length; i++) // do sth here

In Java the length property of an array is static and determined at compile time. That means accessing its value is of O(1) time complexity.

Yet for dynamical arrays (a.k.a vectors in some other languages), using len() method has some overhead. Thus, it is good to store the length in a variable and use it thereafter (assuming the /length/ doesn't change, of course)

int N = someArr.len();
for (int i=0; i<N; i++) // do sth here

Indicating possible failure in initialization

In Rust, when initializing an instance of my own types (usually struct), I can use different function names to indicate different results

struct Student {
    name: &str,
    age: u8,
}

// For sake of simplicity, I ignore lifecycle of &str here
impl Student {
    fn new(name: &str, age: u8) -> Self {
        Self {name, age}
    }

    // Indicate it may fail to initialize a new student
    fn try_new(name &str, age: u8) -> Result<Self, MyError> {
        Ok(Self {name, age})
    }
}

Think about the "forest" all time

When coding, it's easy for me to get obsessed with specific implementations/details/steps only to find I wasted much time later ... It's good to always keep the whole picture in mind. Thus instead of rushing into coding, I should make good plans:

int main()
{
  FirstThing();
  SecondThing();
}

Then work on FirstThing(), and then SecondThing(). A basic plan always helps.

Do one thing at a time

Sometimes it is easy to write some hot code that gets lots of things tangles in one place. For example, in a function several variables or states get updated based on different branches. This often indicates a good time to refactor the code.

To be continued ...

]]>
Mon, 02 Oct 2023 00:00:00 GMT
Confusing Zsh $PATH on macOS https://linerre.github.io/2021/04/27/setting-path-for-zsh-on-macos.html https://linerre.github.io/2021/04/27/setting-path-for-zsh-on-macos.html It is recommended that you read this post on my Gist instead, because there are pretty informative comments and discussions.

TL;DR

Read the sections on path_helper and see if you like Apple's way of managing your PATH.

Baiscally, Apple wishes to adopt a centralized way to manage PATH:

  1. /etc/paths: Contains the base system paths, one per line.
  2. /etc/paths.d/: A directory containing additional path files
    • Each file in this directory contains one or more paths (one per line)
    • Files are processed in alphabetical order
    • Common example: When you install XCode, it adds a file here for developer tools
  3. /etc/manpaths and /etc/manpaths.d/: Similar structure but for managing MANPATH instead of PATH

In doing so, you manage the PATH in arguably one place and use it everywhere (system-wide). Thus, you don't need to set up different "profile" or "env" files. After all, Apple macOS is mainly for personl use. And Apple lovers, if they have more than one devices, tend to use yet another Macbook as well, so they have no such annoying situaton as sync-ing configs between "different" machines.

For Linux users, however, the above approach may turn out to be a pain in the neck.

Note, I stopped using macOS several years ago and haven't ever tried it since then. Apple macOS may have changed?

Zsh initializations

I'm not going to repeat what has alreay been well documented: When Zsh starts up, it looks for a few startup files. Among them, ~/.zshenv and ~/.zshrc are the most relevant to a normal user like me. They are also the focus of this gist.

Both the Zsh User Guide and Arch Wiki suggest that I put environment variables such as $PATH in my ~/.zshenv. I followed the advice and my ~/.zshenv looks like this:

# ########################
# Environment variables  #
# ########################
#
export EDITOR=nvim
export PAGER=less
export ZDOTDIR=$HOME/.config/zsh
export XDG_CONFIG_HOME=$HOME/.config
export KERNEL_NAME=$( uname | tr '[:upper:]' '[:lower:]' )

# remove duplicat entries from $PATH
# zsh uses $path array along with $PATH
typeset -U PATH path

# user compiled python as default python
export PATH=$HOME/python/bin:$PATH
export PYTHONPATH=$HOME/python/

# user installed node as default node
export PATH="$HOME/node/node-v16.0.0-${KERNEL_NAME}-x64"/bin:$PATH
export NODE_MIRROR=https://mirrors.ustc.edu.cn/node/

case $KERNEL_NAME in
    'linux')
        source "$HOME/.cargo/env"
        ;;
    'darwin')
        PATH:/opt/local/bin:/opt/local/sbin:$PATH
        ;;
    *) ;;
esac

As you can see, I prefer to use my own Python and Nodejs under my $HOME while leaving the system ones untouched. Kind of clean in my opinion. And it has been working pretty well on all my Linux/*BSD machines.

The $KERNEL_NAME and case statement is there for zsh to detect which OS I'm on. If I'm on macOS, prepare all that is needed for MacPorts and macOS Nodejs.

Zsh way of $PATH

It can be confusing. Have you ever met:

  1. PATH variable in .zshenv or .zshrc
  2. Weird behaviour with Zsh PATH
  3. zsh can neither find nor execute custom user scripts in ~/bin

The only difficulty that had baffled me was zsh's way of setting $PATH. It is NOT that you can't do export PATH=... stuff, but that Zsh provides an array, $path which is tied to $PATH, meaning that you can either change $PATH using the export convention (changing a scalar string) or change $path (lowercase, an array) and makes it easier to append, prepend, and even insert new paths to the exisiting one.

Over time, $PATH can become quite messy, including a lot of duplicate entries. Thus, $path comes in handy and there are already many mentioning/talking about this. Just to list a few:

  1. Arch Wiki:

The line typeset -U PATH path, where the -U stands for unique, instructs the shell to discard duplicates from both $PATH and $path

  1. Zsh User Guide:

The incantation typeset -U path, where the -U stands for unique, tells the shell that it should not add anything to $path if it's there already.

  1. StackOverFlow:

ZSH allows you to use special mapping of environment variables ... For me that's a very neat feature which can be propagated to other variables.

  1. StaclExchange:

That's useful in that it makes it easier to add remove components or loop over them.

  1. StackExchange:

...benefit of using an array ...

The special macOS

The above $path and $PATH kept working well until I tried to migrate them to my M1 running macOS. If I run echo $PATH or echo $path, I got this:

/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/VMware Fusion.app/Contents/Public:/Library/TeX/texbin:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/opt/local/bin:/opt/local/sbin:/Users/leon/node/node-v16.0.0-darwin-x64/bin:/Users/leon/python/bin

It is weird that even I prepend some paths to $PATH, they end up appearing at the last! On the other hand, the paths for system utilities come first! How come? I searched a lot and finally nailed down the root cause: /usr/libexec/path_helper.

What is /usr/libexec/path_helper?

It is a binary file. No way to see the source code. But as is the case with many unix utilities, it has a man page:

path_helper(8)            BSD System Manager's Manual           path_helper(8)

NAME
     path_helper -- helper for constructing PATH environment variable

SYNOPSIS
     path_helper [-c | -s]

DESCRIPTION
     The path_helper utility reads the contents of the files in the direc-
     tories /etc/paths.d and /etc/manpaths.d and appends their contents to
     the PATH and MANPATH environment variables respectively.  (The MANPATH
     environment variable will not be modified unless it is already set in
     the environment.)

     Files in these directories should contain one path element per line.

     Prior to reading these directories, default PATH and MANPATH values
     are obtained from the files /etc/paths and /etc/manpaths respectively.

     Options:

     -c      Generate C-shell commands on stdout.  This is the default if
             SHELL ends with "csh".

     -s      Generate Bourne shell commands on stdout.  This is the default
             if SHELL does not end with "csh".

NOTE
     The path_helper utility should not be invoked directly.  It is
     intended only for use by the shell profile.

Mac OS X                        March 15, 2007                        Mac OS X
(END)

This has been mentioned in a few places on the Internet too:

  1. Setting system path for command line on Mac OSX
  2. Where is the default terminal $PATH located on Mac?
  3. Where is the default terminal $PATH located on Mac? (Stack OverFlow)

What does this path_helper do?

From the man page and info above, it is pretty clear what this tool does:

  1. it looks for the file /etc/paths and files under /etc/paths.d/
  2. it combines all the paths in these files
  3. it returns the combined paths (as a string) and assigns it to $PATH

It is easy to confirm this. Simply run: $ /usr/libexec/path_helper

And it returns something like:

PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/VMware Fusion.app/Contents/Public:/Library/TeX/texbin:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/opt/local/bin:/opt/local/sbin:/Users/leon/node/node-v16.0.0-darwin-x64/bin:/Users/leon/python/bin";
export PATH

On a fresh macOS, it can be as simple as below:

PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin"
export $PATH

This article, Where PATHs come from, explains pretty much everything about $PATH on macOS.

Here is one more: Mastering the path_helper utility of MacOSX

What does it break?

The only problem, in my opinion, is that path_helper re-orders the $PATH! Where PATHs come from mentioned this:

We have seen path_helper is extremely useful. There is one caveat, however. path_helper may reorder your PATH. Imagine you are pre-pending ~/bin to your PATH because you want to override some standard tools with your own. (Dangerous, but let’s assume you know what you are doing.) Then some process launches a subshell which can call path_helper again. path_helper will ‘find’ your additions to the defined PATH, but it will append to the list of default paths from /etc/paths and /etc/paths.d, changing your order and thus which tools will be used.

Note it will append any user-level path to the default one, which breaks things. This guy was so angry that he thought path_helper is "broken":

@mislav, I understand how it works. It's broken.

I also understand that having it enabled causes problems. You forget the path set in ~/.MacOSX/environment.plist, which is needed for GUI apps, and Terminal.app, being a GUI app, will get that environment before it launches a shell subprocesses.

path_helper reorders the PATH entries set in environment.plist, which causes GUI programs to fail when they execute their scripts via a shell subprocess. A good example is any editor that depends on Exuberant Ctags. path_helper reorders the PATH entries and /usr/bin/ctags gets executed instead of Exuberant Ctags that is installed elsewhere. The editor complains that the wrong ctags is installed.

If I don't set the PATH in environment.plist for GUI apps, then they complain that they can't find programs.

Setting PATH on Mac OS X is a mess, and path_helper doesn't help.

Indeed, I recommend reading the whole thread/discussion, as it provides a great deal of useful information. I'll quote one more here to explain why I don't recommend to modify /etc/paths stuff or diable /usr/libexec/path_helper.

@mislav's this comment argues that:

  1. path_helper does what it is supposed to: generate $PATH for macOS
  2. forget having disabled it might cause further trouble in the future.

NOTE: path_helper also does not help with properly setting up $MANPATH:

  1. OS X: read /etc/manpaths and /etc/manpaths.d #1092
  2. Mac OS X man command ignores $MANPATH (which sucks for HomeBrew installed commands)
  3. path_helper will not set MANPATH

The .*shrc file

Let me ask one more question: why rarely did I find many macOS users complaining about path_helper breaking their Homebrew?

The answer is simple: many, really many, macOS CLI tools ask users to modify their .bashrc or .zshrc, rather than .bash_profile or .zshenv. This is the very key to the solutions to my issue. Read on.

Order is key and everything!

The order in which a shell's startup files get sourced/read matters most! You'd better believe this.

@mislav also has this detailed write-up.

Bash

On macOS, for bash, these files are sourced in the following order:

  1. login mode:
    1. /etc/profile (calling path_helper)
    2. ~/.bash_profile, ~/.bash_login, ~/.profile (whichever exists and found first)
  2. interactive non-login:
    1. /etc/bash.bashrc (some Linux; not on Mac OS X)
    2. ~/.bashrc
  3. non-interactive:
    1. source file in $BASH_ENV

Since macOS always opens a login shell when you start a new terminal window, the /etc/profile always gets sourced. This file is very simple:

/etc/profile

# System-wide .profile for sh(1)

if [ -x /usr/libexec/path_helper ]; then
	eval `/usr/libexec/path_helper -s`
fi

if [ "${BASH-no}" != "no" ]; then
	[ -r /etc/bashrc ] && . /etc/bashrc
fi

After this file, $PATH is set using the contents from /etc/paths and files under /etc/paths.d`.

Then if a user modifies their $PATH in any of ~/.bash_profile, ~/.bashrc, or ~/.profile. $PATH acts as expected. A user may append or prepend to it some other paths:

# prepend a path
export PATH=/some/path:$PATH

# append a path
export PATH=$PATH:/some/path

Zsh

For Zsh, the order is like this:

  1. /etc/zshenv (no longer exists on macOS by default)
  2. ~/.zshenv
  3. login mode:
    1. /etc/zprofile (calling path_helper)
    2. ~/.zprofile
  4. interactive:
    1. /etc/zshrc
    2. ~/.zshrc
  5. login mode:
    1. /etc/zlogin
    2. ~/.zlogin

If you, like me, set $PATH in ~/.zshenv, it gets sourced first and $PATH then becomes something like:

~/.zshenv

# user may append and prepend their paths to the default system one
/some/path:/usr/local/bin:/usr/bin:/bin/....:/some/other/path

Then, on macOS, since it is always login shell in a new terminal window, /etc/zprofile gets sourced and calls path_helper. At this time, path_helper sees your $PATH already contains the default from /etc/paths and /etc/paths.d, it will NOT add anything new to it.

However, it will re-order the $PATH to make it like:

/usr/local/bin:/usr/bin:....:/user/appended/or/prepended/paths

As I have shown in the beginning ... and this, well, breaks things.

Here is an article on login shells: What is Login Shell in Linux?, just in case this confuses you.

Solutions

The key lies in the idea that we should start appending or prepending the $PATH after path_helper has been called.

Using bash, that would be somewhere in ~/.bashrc or ~/.bash_profile.

Using Zsh, on macOS, avoid ~/.zshenv and choose ~/.zshrc or ~/.zprofile instead. In fact, better to replace ~/.zshenv with ~/.zprofile so that both Linux and macOS will use the same files with the very same config.

What if I insist on sticking to Zsh User Guide? See this message from Zsh mailing list: Re: ~/.zshenv or ~/.zprofile

The following two articles might be as helpful:

  1. Moving to Zsh, part 2: Configuration Files
  2. Setting the PATH in Scripts

Using replacements

  1. yb66/path_helper
  2. otaviof/path-helper
  3. mgprot/path_helper

Switching back to Bash

If Zsh doesn't mean anything special to you (or your workflow), especailly when you write shell scripts in a Bash way, why not just Bash then?

]]>
Tue, 27 Apr 2021 00:00:00 GMT