Skip to article frontmatterSkip to article content

A year ago, I switched to the English Colemak layout. I had developed some bad habits with QWERTY, and I found it easier to learn touch typing on a completely new layout than to correct my old muscle memory. While my English typing improved, the switch unfortunately degraded my Arabic typing experience.

This led me to a question: why not design a new, optimal Arabic keyboard layout from the ground up?

This post is a methodological attempt to build one. The final layout depends on the text corpus used for training and the defined costs for finger and hand movements.

Criteria

The design of this keyboard layout is guided by the following principles:

  1. Hand Alternation: Alternate between hands as much as possible to increase typing speed and reduce fatigue.

  2. Home Row Priority: Place the most common letter combinations on the home row for easy access.

  3. Bottom Row Deprioritization: Relegate the least common letters to the less accessible bottom row.

  4. Handedness: Favor one hand over the other (in my case, the right hand).

  5. Adjacent Finger Avoidance: Avoid using adjacent fingers for frequent bigrams (two-letter sequences).

  6. Inward-to-Outward Flow: Prefer typing motions that flow from the outer fingers (pinkie) toward the inner fingers (index and middle).

  7. Finger Strength: Assign more frequent letters to stronger fingers.

To model this problem, we need to define four key components:

Keyboard Representation

We’ll use Keyboard Layout Editor to visualize and define the keyboard.

Keyboard layout

Each key is assigned to a finger, represented by a unique color.

fin2idx = {
    "#da6666": 0, "#95c97e": 1, "#c49a3a": 2, "#8292ff": 3,
    "#d97eea": 4, "#c7ce69": 5, "#67c0e5": 6, "#35c676": 7,
}

class Finger:
    def __init__(self, index):
        self.index = index
				# start from the home key
        self.current_key = index + 10 + (index >= 4)
        self.hand = index // 4
        self.keys = []
        self.costs = {}

    def register_key(self, key):
        self.keys.append(key)

    def precompute_costs(self, theta, phi):
        for key1 in self.keys:
            for key2 in self.keys:
                cost = (
                    theta * (key1.x - key2.x) ** 2
                    + phi[key2.row] * (key1.y - key2.y) ** 2
                )
                self.costs[(key1.index, key2.index)] = cost**0.5

    def __repr__(self):
	        return str(self.__dict__)

The finger starts at its home row key, and every movement to another key has an associated cost. These costs are subjective. For example, moving the pinkie from key 10 to 0 would cost more than moving the index finger from 13 to 3.

Next, we define the Key representation:

class Key:
    def __init__(self, index, x, y, finger):
        self.index = index
        self.x = x
        self.y = y
        self.finger = finger
        self.row = index // 10
        finger.register_key(self)

    def __repr__(self):
        return str(self.__dict__)

First, let’s parse the keyboard layout into our Python script. We’ll use pykle_serial to deserialize the JSON data from the Keyboard Layout Editor and instantiate our Key objects:

# download the json from KLE website above
kbd_data = json.load(open("./keyboard.json", "r"))
kbd = kle.serial.deserialize(kbd_data)

keys = []
for key in kbd.keys:
		# ignore keys without a color
    if key.color in fin2idx:
        keys.append(
            Key(int(key.labels[0]), x=key.x, y=key.y, finger=fin2idx[key.color])
        )

Layout Representation

The layout is an assignment of each character to a group of letters. Here are our initial groups:

GroupL1L2GroupL1L2GroupL1L2
0اأ10ز20ق
1ب11س21ك
2ت12ش22ل
3ث13ص23م
4ج14ض24ن
5ح15ط25ه
6خ16ظ26وؤ
7د17ع27يى
8ذ18غ28ءإ
9ر19ف29ة

Let’s see if we can improve these assignments.

Corpus

I’m going to use a Harakat-free Quran corpus. Let’s look at the letter frequencies.

Letters counts

Now, let’s update our groups to move the less frequent letters to Layer 2 (the shifted layer):

Here is the revised grouping:

GroupL1L2GroupL1L2GroupL1L2
0ا9س18م
1ب10ش19ن
2تث11صض20ه
3ج12طظ21وؤ
4ح13عغ22ي
5خ14ف23إء
6د15ق24ة
7ذ16ك25ىئ
8رز17ل26أآ

Now we will encode the text using these groups:

corpus = "".join(open("quran.txt", "r").readlines()).replace(" ", "").replace("\n", "")
counts_dict = dict(sorted(collections.Counter(corpus).items(), key=lambda x: x[1]))
groups = {
    "ا": 0, "ب": 1, "ت": 2, "ث": 3, "ج": 4, "ح": 5, "خ": 6, "د": 7, "ذ": 8, "ر": 9,
    "ز": 10, "س": 11, "ش": 12, "ص": 13, "ض": 14, "ط": 15, "ظ": 15, "ع": 16, "غ": 16,
    "ف": 17, "ق": 18, "ك": 19, "ل": 20, "م": 21, "ن": 22, "ه": 23, "و": 24, "ؤ": 24,
    "ي": 25, "إ": 26, "ء": 26, "ة": 27, "ى": 28, "ئ": 28, "أ": 29, "آ": 29,
}
codified = []
for c in corpus:
    codified.append(groups[c])

Layout Evaluation

Each layout is a permutation of 30 keys and 30 character groups, assigning each group to a key. Each group inherits the attributes of its assigned key (finger, row, coordinates, hand).

A key detail is that a finger’s position is reset to its home key if it hasn’t been used in the last three keystrokes.

We evaluate each permutation based on an “effort measure,” which we define as follows:

Cost(Ft,Kt,Ft1)=[1+α(Ft1,Ft)].β(Ft).d(Ftp,Ktp)Cost(F_t,K_t, F_{t-1}) = \left[1+\alpha(F_{t-1},F_t)\right].\beta(F_t) .d(F_t^p, K_t^p)

Cost breakdown

α(X,Y)={0ifX.handY.handAX,Yotherwise\alpha(X,Y) = \begin{cases}0 & \text{if} & X.hand \neq Y.hand \\ \Alpha_{X,Y} && \text{otherwise} \end{cases}
# gamma * np.abs(np.arange(4)[:, None] - np.arange(4)[None, :])
alpha_left = gamma * np.array([
  [1.5, 1,   0.8, 0.6],
  [1,   1.2, 1,   0.8],
  [0.8, 1,   1.5, 1.2],
  [0.8, 0.6, 1,   1.5]
])
# define the larger alpha
alpha = np.block([[alpha_left, np.zeros((4, 4))], [np.zeros((4, 4)), alpha_left]])
d(F,K)=θ.(F.xK.x)2+[1+ψ(K.row)].(F.yK.y)2d(F, K) = \sqrt{\theta.(F.x-K.x)^2 + [1+\psi(K.row)].(F.y-K.y)^2}

We have 9 symmetric (or 13 total) hyperparameters to tune:

To avoid redundant computations, we pre-compute the term β(Ft)d(Ftp,Ktp)\beta(F_t) \cdot d(F_t^p, K_t^p) for all finger-key pairs after selecting the hyperparameters.

Optimization: Simulated Annealing

We use simulated annealing to find the optimal layout:

  1. Start with a random permutation p and an initial temperature T0T_0.

  2. Evaluate p to get its cost c.

  3. For each iteration t[1,iterations]t \in [1, \text{iterations}]:

    • a. Pick a random integer n[1,b]n \in [1, b] (bb is search depth)

    • b. For n times, make a random modification to p (e.g., swap two items or two columns).

    • c. Evaluate the modified permutation pnewp_{\text{new}} to get its cost cnewc_{\text{new}}.

    • d. Sample a random number uU(0,1)u \sim \mathcal{U}(0,1).

    • e. If exp(cnewcT)u\exp(-\frac{c_{\text{new}}-c}{T}) \ge u, accept the new permutation.

    • f. Decrease the temperature: TT0(1t/k)T \gets T_0 (1 - t/k).

Layout Optimization

Hyperparameters

Standard Arabic Layout: Cost 866,184

Standard layout

Optimized Layout: Cost 481,506

This layout achieves a 45.6% reduction in effort compared to the standard layout.

Optimized

Modified Optimal Layout (with Key Symmetry)

This version has a cost of 501,631, a 43% improvement over the standard layout. It’s a cleaner design that places similar-sounding or related letters on symmetric keys (e.g., ق/ف, س/ش).

Cleaner layout

You can try it out here: Link to layout

Future Work

This analysis doesn’t yet account for keyboard layers. Moving from one layer to another (e.g., by pressing Shift) incurs a “layer switch” cost that should be incorporated into the model for a more complete optimization.