I design user interfaces and program software. Also I seem to have a blog now.
Blog RSS Mastodon

Recent Posts

Collection: X11 I3wm Setup

I3dropdown: Animated Drop Down for Any Window in i3wm

Published

One of the first additions to my tiling window workflow for my linux desktop was a drop-down terminal; Originally with a dedicated terminal application, then with a shell script with urxvt and then finally my own dedicated program, i3dropdown. Which provides drop-down functionally to in any program and has great performance.

Similar to Evdoublebind discus in my last post, i3dropdown was design to be used for i3wm on X11 and when I migrate to wayland the functionally provided by i3dropdown will have to be duplicated. However, since i3dropdown works entirely by interactions with i3wm's IPC which sway provides backwards compatibility with, migration to sway should be easy for this application.

How I Use Drop Downs With I3dropdown

Most frequently I use terminals, chat-programs like mumble and my password manager as drop downs. For mumble and my password manager is have dedicated keys S-m and S-p respectively which when pressed toggle the application, dropping down the application on my current display and starting the application if closed or hiding the window it was already visible. Although, i3dropdown works great with multi monitor setups I find it most useful when I am on the go using my 14 inch laptop, as I have very little screen real estate.

Further I can make any application a drop-down by pressing one of the keys H-TAB, H-1, H-2 and H-3 which will convert my focused window into a drop down and hide it assigning it the key used so further presses of that key toggles that window. Usually these auxiliary drop-downs are reference material in a PDF viewer or terminals used for compilation output needing little interaction like LaTeX.

I also have a main terminal where run as Tmux session that I use for many things and a dedicated file-manager terminal currently running nnn bound to S-n which hides it self when opening an file with a graphical application. Since I utilize urxvt's daemon feature dropping down a terminal is instant even from a closed state. Further I use 7 frame only drop animation, just long enough to keep it smooth.

My configuration to use I3dropdown for my terminal is shown below, the reset of my configs follow a similar pattern.

#FILE:.config/i3/config
for_window [instance="ddterminal"] floating enable;
bindsym Mod3+Return exec --no-startup-id i3dropdown ddterminal \
    "urxvtc -name ddterminal -background rgba:1000/1000/1000/D000 \
    -internalBorder 4 -e dash -c 'tmux new -t dropdown'"

How I3dropdown Works

At the highest level, I3dropdown detects which state the application window is in: closed, open on active display, or other. Then, if closed the application is started and displayed on the active display, if already on the active display the application is hidden and if hidden it is displayed. To gather all the information needed, I3dropdown uses the I3 IPC to get the JSON tree output of I3. From the tree, the current display, it's resolution and the application state is determined by searching for window matching the instance name provided as a command line argument. When necessary the application is started using the command provide also as a command line argument. Then using the i3IPC the application window is position and resized as necessary to provide the animating drop down effect.

An Adventure in Extreme Optimization

When creating I3dropdown I spent a lot of time optimizing it, more for fun and learning rather than for practical purposes. I3dropdown is definitely pre-maturely optimizated and currently contains many optimizations each of which fall into one of the three following categories in terms of fruitfulness:

  1. Reducing an observable amount of latency. These are the most important optimizations often reducing the number of frames for the application to be displayed. Such optimizations include:
    • Not using i3 --get-socketpath to get ipc-socket, which can take 10ms.
    • Interacting with i3wm's IPC directly instead of i3-msg which suffers from the point above.
    • Avoiding both Xrandr or connecting to the X server at all, which can take 6ms
  2. Reducing measurable amount of latency. Well usually these result in no observable change combined they might. Such optimizations include:
    • Using a statically compiled dash with musl for the execution environment for starting applications instead of bash.
      #Note task-clock is not the same as real elapsed time.
      > sudo perf stat dash -c "echo hi" 
      ... 0.09 msec task-clock          # 0.204 CPUs utilized
      # dash vs bash
      > sudo perf stat bash -c "echo hi" 
      ... 2.67 msec task-clock          # 0.909 CPUs utilized
      
    • Statically compiling i3dropdown with musl.
  3. Reducing measurable total of cycles/instructions/task-clock of i3dropdown. Well "fun" this resulted in no observable improvement nor any meaningful impact of total system utilization since the application had so little impact in the first place. The main optimization in the category was how JSON interactions was handled, discussed latter. However, such optimizations did provided bragging rights that i3dropdown's total user space execution resources are less than what it takes to dynamically link and start a binary, as seen below with perf stat comparing with /bin/echo.
    > perf stat i3dropdown ddterminal ~/scripts/dropdown_urxvt.sh
    ...
              7      page-faults:u        #    0.010 M/sec
        102,520      cycles:u             #    0.145 GHz
         83,926      instructions:u       #    0.82  insn per cycle
         17,924      branches:u           #   25.302 M/sec
    # i3dropdown vs /bin/echo
    > perf stat /bin/echo hi
    ...
             57      page-faults:u        #    0.132 M/sec
        355,561      cycles:u             #    0.821 GHz
        237,662      instructions:u       #    0.67  insn per cycle
         50,255      branches:u           #  116.021 M/sec
    

My biggest gripe with all the extreme optimization is it didn't even produce the fastest solution, which would be to make a patch and just implement drop-downs directly in i3. However, having i3dropdown as a separate program has perks too especially if I can adapt it to work on sway which supports i3wm's IPC.

Brief Introduction to Prediquery and How I3dropdown Uses It

Prediquery was designed for extracting a small amount of data out of a large amount of json. Specifically I3dropdown uses Prediquery to extract the size of the display and position/state of a application matched by instance name from i3 IPC tree output. I'm going to provide just enough detail about prediquery here to understand how i3dropdown uses it, as it deserves a post of it's own.

The basic idea is to, with one pass of the JSON, without mutating the json be able to perform a query that can find objects that match as set of predicates in a possibly recursive json structure and then extract values from the objects fields. The predicates are Boolean expressions of other predicates or functions taking inputs of child or parent objects of the a possible matching object.

Before I explain any more lets consider a very simple example with a code snippet from i3dropdown.

struct rect_box {
   long int width, height, x, y;
};

rect_box rect_parse(char * json){
    rect_box box;
    using namespace prediquery;
    QueryExpr query[] = {
        /*0*/invoice("x", &box.x),
        /*1*/invoice("y", &box.y), 
        /*2*/invoice("width", &box.width), 
        /*3*/invoice("height", &box.height)
    };
    uint64_t res = tquery(json, query, 4 /*length of query[]*/, bitset(0, 1, 2, 3));
    if (res != bitset(0,1,2,3)){
        printf("rect parse error");
    }
    return box;
}

In the snippet Prediquery used to perform a basic parse of JSON input in the form {"x":0,"y":1049,"width":1920,"height":31}. The invoice directive in a query specifies a field value of an object, where to store it's value and a bitset of required predicates to save that value. In the above snippet no required predicates are provided to the invoice so they are always enabled if active. Then tquery(json, query, 4, bitset(0, 1, 2, 3)) provides the json, the query, the number of query directives in the query and the initial query directives active in root of the JSON structure.tquery returns a bitset of directives fulfilled.

Eventually if I continue work on prediquery I want to avoid having users write query directives by hand, as you'll seen in a moment they become complex and error prone quickly.

The query below is defined recursively. The recurse directive indicates the set of active query directives to be used when parsing the object or array of objects at a field, if the field neither exists nor is an object or array of objects it is ignored.

i3Query queryi3(const char * inst, const char *reply){
    char *output_rect = nullptr;
    int64_t instance_workspace = 0;
    int64_t current_workspace = 0;
    uint64_t instance_id = 0;

    using namespace prediquery;
    const QueryExpr query[] = {
       /*00*/recurse("nodes", bitset(1, 2, 3)), //Output Nodes
       /*01*/invoice("rect", &output_rect, bitset(2)),  
       /*02*/dummy(bitset(1, 6), raise_flag(2)), //FOUND Focus Dummy Flag

       /*03*/recurse("nodes", bitset(2, 4, 11)), //{topdock, *content*, bottomdock} Nodes

       /*04*/recurse("nodes", bitset(2, 5, 6, 7, 8, 9, 11)), //Workspaces Nodes
       /*05*/invoice("num", &instance_workspace, bitset(7)), 
       /*06*/invoice("num", &current_workspace, bitset(2, 11), EXPR_FLAG::OR),
       /*07*/dummy(bitset(5), raise_flag(7)), //FOUND Instance Dummy Flag

       /*08*/recurse("floating_nodes", bitset(2, 7, 8, 9, 10, 11, 12)), //Container Nodes
       /*09*/recurse("nodes",          bitset(2, 7, 8, 9, 10, 11, 12)), //Container Nodes
       /*10*/invoice("id", &instance_id, bitset(13)), 
       /*11*/predicate("focused", true, bitset(6), EXPR_FLAG::BOOL | raise_flag(2)),

       /*12*/recurse("window_properties", bitset(13)), //Container Nodes window properties
       /*13*/predicate("instance", inst, bitset(10, 7),
                       EXPR_FLAG::STRING_PREFIX | EXPR_FLAG::RAISE_RELATED)
    };

    uint64_t stub = tquery(reply, query, 14 /*length of query[]*/, bitset(0));

    if ((stub & bitset(1, 6)) != bitset(1, 6)){
        fprintf(stderr,"Could not find active output/workspace\n");
        exit(1);
    }

    return i3Query{
        .instance_workspace = instance_workspace,
        .current_workspace = current_workspace,
        .instance_found = (stub & bitset(10, 5)) == bitset(10, 5),
        .instance_id = instance_id,
        .output = rect_parse(output_rect)
    };
}

Since when writing the query and checking the results you use the index of directives in the query array it encourages the use of magic numbers, is error prone and hard to make change. Although the performance is quite good for my and similar use cases, large json and small amount of extracted data required, I can't recommend Prediquery yet as really just a prototype.

Ideally a compile-time intermediate language would be written that if expressive enough would make Prediquery both easier and faster than parsing the JSON and walking the tree your self.