Konfiguracja DWM: przywołać konsolę

Jak już kilkakrotnie wspominałem, jako menedżer okien, preferuję DWM, bo projekt zakłada minimalną implementację Kaskadującego zarządzania oknami w języku C, a cała konfiguracja odbywa się przez modyfikację kodu źródłowego (dla prostych zmian wystarczy modyfikacja jednego pliku nagłówkowego z konfiguracją). W tym odcinku chciałbym opisać kolejną zmianę w DWM, wraz z implementacją, jako kolejny przykład tego, jak prosto zmodyfikować DWM do własnych potrzeb. Będę pracować na moim forku (commit 465fe27) aczkolwiek, jednak można też użyć oryginału. Tym razem trochę bardziej wnikniemy w najważniejsze elementy kodu DWM i zobaczymy jak zaimplementować konkretne zmiany.

w tym artykule:

  1. Problem
  2. Najważniejsze elementy kodu DWM
  3. 1. sprawa: pogrupuj wszystkie terminale
  4. 2. i 3. sprawa: j/w z vimem i Firefoxem
  5. 4. sprawa: szybki wybór spośród okien
  6. Podsumowując

Problem

Gdy pracuję, przeważnie większość okien jakie mam to terminale (tu używam akurat funkcji wypisywania okien, z mojego forka):

[lew@T430 ~]$ printf 'l' > /tmp/dwm.cmd
[lew@T430 ~]$ cat /tmp/dwm.out 
lew@T430:~
BartekLew/dwm at 465fe2749b62ec25c9390eadce43af3c10755383 - Mozilla Firefox
Steam
lew@T430:~/muzyka
Lista znajomych
Wine System Tray
Steam
dwm-cc1.txt + (~/wiedz.net.pl/it/misc) - VIM
lew@T430:~
lew@T430:~
lew@T430:~/vids

Dużo z tym zabawy. Nie pamiętam co jest na którym pulpicie. Dlatego chciałbym:

  1. Przerzucić wszystkie terminale na jeden pulpit i wyświetlić w trybie kaskady (ekran podzielony tak żeby wyświetlić wszystkie).
  2. Jak w punkcie 1., ale tylko okna z vimem.
  3. Jak w punkcie 1., ale tylko okna Firefoxa.
  4. W trybie kaskadowym, wybrać okno jedno i nastawić je na pełnen ekran jednym skrótem klawiszowym.

Najważniejsze elementy kodu DWM

Struktura obiektów okien

Najbardziej będzie nas interesować struktura reprezentująca okno:

struct Client {
 char name[256];
 float mina, maxa;
 int x, y, w, h;
 int oldx, oldy, oldw, oldh;
 int basew, baseh, incw, inch, maxw, maxh, minw, minh;
 int bw, oldbw;
 unsigned int tags;
 int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen;
 Client *next;
 Client *snext;
 Monitor *mon;
 Window win;
};

name przechowuje tytuł okna gotowy do wyświetlenia. tags: to mapa bitowa która mówi, na których pulpitach je widać (mogą być widoczne na wielu). Okna są ułożone w listę, więc next oznacza następne okno. Pole win przechowuje obiekt, którego możemy użyć przy użyciu Xlib. Pozostałe pola raczej nie będą nam potrzebne.

Okna są pogrupowane w monitory (w sensie fizycznym, o ile wyświetlają co innego) Listę wszystkich okien w bieżącym monitorze znajdziemy w selmon->clients (dostęp globalny). Można też pobrać wszystkie:

for(Monitor *mon = mons; mon; mon = mon->next){
    for(Client *c = mon->clients; c; c = c->next) {
        // …
    }
}

Gdy coś zmieniamy, żeby wywołać zmianę, należy użyć funkcji arrange(Monitor*).

Skróty klawiszowe

Kaskadujące zarządzanie oknami, z zasady opiera się o skróty klawiszowe. Lista skrótów klawiszowych powinna znajdować się w pliku config.h, który nie istnieje w repozytorium, istnieje za to wzór config.def.h. Znajdziemy w nim tablicę keys, która wygląda tak (fragment):

static Key keys[] = {
 /* modifier                     key        function        argument */
 { MODKEY,                       XK_p,      spawn,          {.v = dmenucmd }
 },
 { MODKEY|ShiftMask,             XK_Return, spawn,          {.v = termcmd } 
},
 { MODKEY,                       XK_b,      togglebar,      {0} },
 { MODKEY,                 XK_Cyrillic_i,   togglebar,      {0} },
 { MODKEY,                       XK_j,      focusstack,     {.i = +1 } },
 { MODKEY,                 XK_Cyrillic_o,   focusstack,     {.i = +1 } },
 { MODKEY,                       XK_k,      focusstack,     {.i = -1 } },
 { MODKEY,                 XK_Cyrillic_el,  focusstack,     {.i = -1 } },

Pierwsze pole oznacza wciśnięte klawisze modyfikujące (shift, ctrl, alt, itp). MODKEY jest podstawowym klawiszem komendy definiowanym wcześniej w tym pliku. W oryginalnym DWM jest to bodaj alt, u mnie klawisz znajdujący się zazwyczaj między ctrl i alt.

Drugie pole oznacza klawisz, w postaci strawnej dla Xlib, skrótu. W załączonym kawałku kodu możemy zobaczyć również znaki cyrylicy, ponieważ używam również rosyjskiego układu klawiatury, a te wartości są zależne od układu klawiatury.

Trzecie pole to nazwa funkcji użytej do obsługi klawisza. Czwarte zaś pole argument, który będzie do niej przekazany (jak można się domyśleć, jest to unia). Na przykład funkcja, którą zdefiniowałem dla zmiany układu klawiatury wygląda tak:

void
keymap(const Arg *arg)
{
    if (fork() == 0) {
        if (dpy)
            close(ConnectionNumber(dpy));
        setsid();
        execlp("setxkbmap", "setxkbmap", arg->s, (char*) NULL);
        fprintf(stderr, "dwm: cannot set keymap: %s", arg->s);
        perror(" failed");
        exit(EXIT_SUCCESS);
    }
}

zaś definicje klawiszy wyglądają tak:

    { MODKEY|ShiftMask,        XK_l,       keymap,       {.s = "ru"} },
    { MODKEY|ShiftMask,       XK_Cyrillic_de,  keymap,       {.s = "pl"} },	

Czyli MOD+Shift+l/MOD+Shift+д (ten sam klawisz). Niestety, żeby dodać do tego, na przykład, ukraiński układ klawiatury, musiałbym zaimplementować coś bardziej złożonego. Chyba wolę rozszerzyć definicję rosyjskiego układu klawiatury, żeby jednocześnie obsługiwał ukraiński.

Pętla kominunikatów

Czasem potrzebujemy dodać jakąś inicjalizację, dodać nowy element do pętli komunikatów. Wówczas interesuje nas funkcja run().

1. sprawa: pogrupuj wszystkie terminale

Rozłóżmy sobie działanie na podstawowe czynności: otwórz pusty pulpit, znajdź okna według wzorca, umieść wybrane okna na tym pulpicie. Zatem chcę, żeby nasza funkcja wyglądała jakoś tak:

void
gather(const Arg* arg) {
    int etag = temp_tag();
    size_t len = strlen(arg->s);

    for(Client *c = selmon->clients; c; c->next) {
        if(strncmp(arg->s, c->name, len))
            c->tags |= 1 << etag;
    }

    choose_tag(etag);
}

Pusty pulpit

Pomysł na pusty pulpit jest prosty. Dodam po prostu jeden pulpit, do którego nie podłączymy żadnych skrótów klawiszowych. W config.h:

/* tagging */
static const char *tags[] = {
    "1", "2", "3", "4", "5", "6", "7", "8", "9", "tmp"
};
#define TMPTAG 9

Aby się nie wyświetlał, wystarczy w funkcji drawbar(linia 732), zmienić LENGTH(tags) na TMPTAG. Dzięki temu ten jeden pulpit nie będzie się pokazywać.

Wówczas:

void
temp_tag(void) {
    for(Client *c = selmon->clients; c; c = c->next) {
        c->tags &= ~TMPTAG;
    }

    return TMPTAG;
}

Wybranie pulpitu

Dość łatwo dojść, jak zmienić pulpit, bo jest to działanie przyporządkowane do skrótu klawiszowego. W tablicy keys widzimy, że te pozycje są definiowane przez makro:

    TAGKEYS(                        XK_1,                      0)
    TAGKEYS(                        XK_2,                      1)
    TAGKEYS(                        XK_3,                      2)
    TAGKEYS(                        XK_4,                      3)
    TAGKEYS(                        XK_5,                      4)
    TAGKEYS(                        XK_6,                      5)
    // itd…

Patrząc na makro, widzimy, że to dlatego, że dla każdego pulpitu definiujemy aż cztery skróty klawiszowe:

#define TAGKEYS(KEY,TAG) \
    { MODKEY,                       KEY, view,       {.ui = 1 << TAG} }, \
    { MODKEY|ControlMask,           KEY, toggleview, {.ui = 1 << TAG} }, \
    { MODKEY|ShiftMask,             KEY, tag,        {.ui = 1 << TAG} }, \
    { MODKEY|ControlMask|ShiftMask, KEY, toggletag,  {.UI = 1 << tag} },

Z uruchomionym DWM, możemy łatwo sprawdzić, że odpowiednio funkcje oznaczają: wybierz pulpit, wyświetlaj jednocześnie też okna z pulpitu, przenieś (bieżące) okno na pulpit i dodaj okno do pulpit (nie usuwając go z bieżącego). Możemy z tego łatwo wywnioskować, że interesuje nas funkcja view(). Przyznaję, że nie jest to wzór czystego kodu. Nie rozumiem dokładnie kodu. Wystarczy mi jednak, że będę znał numer pulpitu. Mając dany pulpit, wystarczy zrobić:

view(&(const Arg){.ui = 1 << emon->num});

Podobnym sposobem możemy wydedukować, że dla zapewnienia układu kaskadowego, powinniśmy dodać jeszcze:

setlayout(&(const Arg){.v = &layouts[0]});

Podsumowując

Tak więc ostatecznie kończymy z kodem:

void
temp_tag(void) {
    for(Client *c = selmon->clients; c; c = c->next) {
        c->tags &= ~TMPTAG;
    }

    return TMPTAG;
}

void
gather(const Arg* arg) {
    int emon = temp_tag();
    for(Client *c = selmon->clients; c; c->next) {
        if(strstr(c->name, arg->s))
            c->tags |= 1 << emon;
    }

    view(&(const Arg){.ui = 1 << emon);
}

a zasadniczo wystarczy tak:

void
gather(const Arg* arg) {
    for(Client *c = selmon->clients; c; c->next) {
        if(strstr(c->name, arg->s))
            c->tags |= 1 << TMPTAG;
        else
            c->tags &= ~TMPTAG;
    }

    view(&(const Arg){.ui = 1 << TMPTAG});
    setlayout(&(const Arg){.v = &layouts[0]});
}

dodając jednocześnie prototyp w dwm.c, przed załączeniem config.h. Następnie wystarczy dodać skrót klawiszowy:

    { MODKEY|ShiftMask,       XK_t, gather, {.s = TERM_PATTERN}},

Przy czym TERM_PATTERN to początek tytułu terminala na powłoce, u mnie "lew@T430:".

Cała zmiana: tutaj.

2. i 3. sprawa: j/w z vimem i Firefoxem

Proste:

    { MODKEY,                       XK_v,    gather,     {.s = "- VIM"}},
    { MODKEY|ControlMask,           XK_f,    gather,     {.s = "- Mozilla Fi
refox"}},

4. sprawa: szybki wybór spośród okien

Wybrane pulpity (można wyświetlać kilka na raz) możemy pobrać poprzez selmon->seltags, więc przeiterować po widocznych oknach możemy tak:

int tags = selmon->seltags;
for (Client *c = selmon->clients; c; c = c->next) {
    if((c->tags & tags) == tags) {
        //…
    }
}

Typowym sposobem wyboru w DWM, jest drugi program, tego samego autora: dmenu. Rozwiązanie do bólu proste: na standardowe wejście wpisuje się opcje do wyboru, po zamknięciu strumienia wejścia wyświetla się wybór i, po wybraniu opcji, wybór jest wypisywany na wyjściu. Żeby nie pisać na nowo, użyję funkcji stąd, tylko dorobię do niej możliwość użycia printf. Otrzymamy więc:

static void
choose(const Arg *arg) {
    (void) arg;
    //don't do it if there are no windows to choose
    if(!selmon->sel) return;

    StreamSet streams = meanwhile(&execute, (char*[]){"dmenu", NULL});
    if(streams.in == NULL)
        fprintf(stderr, "error running dmenu");

    int tags = selmon->seltags;
    for (Client *c = selmon->clients; c; c = c->next) {
        if((c->tags & tags) == tags) {
            fprintf(streams.in, "%p %s\n", (void*)c, c->name);
        }
    }

    fclose(streams.in);

    Client *c;
    if(fscanf(streams.out, "%p", (void**)&c) == 1) {
        if(c) {
            focus(c);
            arrange(selmon);
        }
    } else {
        fprintf(stderr, "couldn't read from dmenu");
    }

    fclose(streams.out);
    fclose(streams.err);
}

Już mogę dodać sobie takie działanie, bardzo przydatne. Pozostaje dodać jeszcze wywołanie tej funkcji w gather.

Cała zmiana tutaj.

Podsumowując

Jak widać, pomimo, że DWM napisany jest w C, całkiem łatwo i szybko (wszystkiego ok 30 linijek jeśli nie liczyć sporego kawałka kodu przeklejonego z innego projektu) można dodać do niego nowe operacje. Niby trzeba ogarnąć jak działa, ale za to nie trzeba uczyć się żadnego DSLa czy wymyślnego API w jakimś wyszukanym języku programowania. Dla mnie, ma to bardzo dużą wartość, bo dzięki temu wpadam na usprawnienia pracy, o których bym nie pomyślał, bo nie wiem, że tak można. Tu wiem, że mogę wszystko na co pozwala C w userspace i niewiele trzeba wysiłku, żeby zacząć.