Viime blogisarjan osasta onkin vierähtänyt jo hetki. Tuolloin työstimme Emacsistamme Python-IDE:n, tutustuimme Flycheck-syntaksikääntäjän antamiin varoituksiin ja hioimme editorin käytettävyysasetuksia. Jatkamme myöhemmissä blogin osissa Python-konfiguraatioidemme hiomista. Tämänkertaisessa tekstissä opettelemme sen sijaan käyttämään terminaalia editorin sisältä. Näin Emacsista toiseen ikkunaan vaihtamiselle tulee yhä harvemmin tarvetta.
Terminaalivaihe
Emacsiin sisältyy sisäänrakennettuna muutamiakin komentoja shellien ajamiseen ja terminaalin emulointiin. Yksinkertaisin ja vanhin näistä on M-x shell, jolla shellin voi ajaa aliprosessina Emacs-bufferissa. Shell-bufferi käyttää pohjanaan Emacsin Comint-moodia, joka tulee sanoista command-interpreter-in-a-buffer.

Shell-bufferin tarjoamat terminaaliominaisuudet ovat hyvin rajoitetut: aliprosessi näkee käyttäjän syötteen vasta, kun hän painaa enteriä, eikä esimerkiksi kursorin liikuttamista ympäriinsä tueta. Tämän vuoksi monet kokoruudun sovellukset (top, less, jne.) toimivat puutteellisesti eikä ruudun puhdistaminen clear-käskyllä onnistu.
Yksittäisiä shell-komentoja voi myös ajaa kätevästi komennolla M-! cmd RET, jossa cmd on ajettava komento ja RET enter-näppäimen painallus, tai vaihtoehtoisesti komennolla M-x shell-command. Asynkronisesti shell-komennon ajaminen onnistuu komennoilla M-& cmd RET tai M-x async-shell-command.
Kehittyneempi versio Emacsin shell-komennosta on term-komento eli M-x term. Sen luomissa buffereissa on käytössä täysi terminaaliemulaatio, mikä paikkaa shell-bufferien edellä mainittuja puutteita.
Lisäksi term-bufferi tarjoaa kaksi erilaista moodia: char-moden ja line-moden, joista oletuksena on päällä char-mode. Char-modessa bufferi tulkkaa syötteitä merkki kerrallaan, kuten mikä tahansa muukin terminaali. Line-modessa taas term-bufferi toimii hyvin samalla tavalla kuin shell-bufferi, eli syötteitä tulkitaan rivi kerrallaan. Tämä mahdollistaa joustavan terminaalin syötteiden ja tulosteiden käsittelyn Emacsin omin komennoin, koska terminaalin omat näppäinyhdistelmät eivät yliaja Emacsin näppäinyhdistelmiä. Vastaavasti taas char-modessa onnistuu esimerkiksi nano-editorista poistuminen, koska Emacs ei yliaja C-x-yhdistelmää. Char-modesta line-modeen voi vaihtaa näppäinyhdistelmällä C-c C-j ja line-modesta char-modeen yhdistelmällä C-c C-k.

Huomionarvoisena puolena term-bufferin ollessa char-moodissa terminaaliprosessin näppäinyhdistelmät ovat konfliktissa Emacsin omien näppäinyhdistelmien kanssa. Siten esim. Ctrl- tai Alt-näppäimen painallus tulkitaan kuin ne alla ajautuvassa shellissä tulkittaisiin. Siksi Term-moodi vaihtaa Emacsin oman C-x-näppäinyhdistelmän char-moodissa C-c:ksi, jotta Emacs-pikakomentoja on mahdollista suorittaa.
Term-komennolle toinen vaihtoehto on M-x ansi-term. Ennen ero näiden kahden välillä oli se, että ainoastaan ansi-term tuki värien renderöintiä. Nykyään term-bufferit tukevat sitä kuitenkin myös, joten ero näiden kahden välillä on olematon.
Emacsin kuorintaa
Edellä mainituilla työkaluilla terminaalin käyttö onnistuu jo varsin hyvin. Jatkuvaa käyttöä varten lisämukavuudet eivät kuitenkaan ole pahitteeksi. Yksi vaihtoehto, jonka Emacs tarjoaa sisäänrakennettuna, on kokonaan Elispillä toteutettu Eshell. Se ei siis aja aliprosessina esimerkiksi bashia vaan on itsessään shell-ohjelma.
Eshellin suurin etu on siinä, että se pystyy käyttämään natiivisti Elisp-lauseita ja Emacs-objekteja osana shell-komentoja. Esimerkiksi komennon tulosteen pystyy ohjaamaan suoraan Emacs-bufferiin:
echo "hello world" > #<buffer hello>
Seuraava komento avaa puolestaan tiedoston suoraan Emacs-bufferina:
find-file init.el
Näin taas avataan /var/log-hakemiston sisältö Emacsin dired-hakemistoeditorissa:
dired /var/log
Monet Emacs-työkalut toimivat myös automaattisesti yhdessä Eshellin kanssa. Esimerkiksi tabulaattoritäydennys eshellissä osaa hyödyntää Helmiä täydentämiseen. Samoin diff-komennon ajaminen avaa Emacsin oman Diff-bufferin.

Alla olevassa kuvassa on puolestaan esitelty Elisp-funktion käyttöä komentoriviltä. Käyttäjä voi määritellä myös omia funktioitaan. Elisp onkin tehokas työkalu, koska se on huomattavasti näppärämpi skriptikieli kuin Bash.

Eshell pystyy hyödyntämään suoraan myös Emacsin sisäänrakennettua etäyhteyksiin käytettävää Tramp-pakettia, johon tutustumme myöhemmissä blogisarjan osissa. Alla alert on lisäksi alert-paketin tarjoama Elisp-funktio, joka antaa notifikaation, kun etänä ajettavat testit ovat suoriutuneet:
cd /ssh:remote:/home/miikka npm test && alert "remote tests are done"
Eshellin käyttämisessä on siis selkeät etunsa mutta niin ovat myös haittapuolet. Se on suorituskyvyltään selvästi hitaampi kuin Bash-pohjainen terminaali, mikä voi muodostua ongelmaksi erityisesti suurten tulosteiden kanssa. Lisäksi niin sanotut visuaaliset komennot, jotka eivät ole vain rivipohjaisia vaan tarvitsevat erillisen terminaalin tulosteensa esittämiseen, täytyy lisätä erikseen eshell-visual-commands tai eshell-visual-subcommands-listoihin. Tämä johtuu siitä, ettei Eshell ole terminaali vaan shelli. Esimerkiksi seuraava init.eliin lisättävä Elisp-pätkä lisää komennon git log visuaalisiin alikomentoihin:
(add-to-list 'eshell-visual-subcommands '("git" "log"))
Eshell ei myöskään ole täysin yhteensopiva Bash-syntaksin ja -komentojen kanssa. Esimerkiksi $( ) on kirjoitettava Bashissa aaltosulkein ${ }, eikä muun muassa syötteen uudelleenohjausta tueta. Siten vaikkapa komennon wc < file sijaan on käytettävä komentoa cat file | wc. Tämä aiheuttaa lisävaivaa, sillä Bashin käyttöön on luonnollisesti helpompi löytää internetistä tukea ja esimerkkejä kuin Elispin.
Omassa käytössäni olen todennut Eshelliä pidemmänkin aikaa kokeiltuani, että Bashia tai muuta shelliä allaan ajavat terminaalit aiheuttavat sitä vähemmän harmaita hiuksia. On myös hyvä pitää mielessä, ettei Eshelliä ole edes sen dokumentaation mukaan tarkoitettu Bashin kaltaisten shellien korvikkeeksi. Jos kuitenkin opettelun intoa riittää, Eshellistä saa kyllä tehokkaan työkalun, mitä varten voi perehtyä seuraaviin hyödyllisiin lähteisiin:
https://masteringemacs.org/article/complete-guide-mastering-eshell
https://www.gnu.org/software/emacs/manual/html_node/eshell/
https://www.emacswiki.org/emacs/CategoryEshell
http://www.howardism.org/Technical/Emacs/eshell-present.html
Moniterminen valinta
Oma valintani Emacs-terminaaliksi on jo pitkään ollut multi-term-paketti. Sen pystyy ottamaan käyttöön init.elin välityksellä seuraavasti:
(use-package multi-term :ensure t)
Multi-term on Term-moodin päälle rakennettu paketti, joka tuo kuitenkin sen päälle muutamia elämää helpottavia lisäominaisuuksia. Näistä tärkeimmät ovat helppo uusien terminaalibuffereiden luonti ja niiden välillä vaihtaminen sekä näppäinyhdistelmät, jotka eivät aiheuta konfliktia Emacsin omien tärkeimpien globaalien näppäinyhdistelmien kanssa.

Multi-termillä voi avata Term-moodissa olevan bufferin komennolla M-x multi-term. Tätä samaa komentoa käytetään myös avaamaan lisää terminaali-ikkunoita. Lisätään tälle komennolle globaali pikanäppäinyhdistelmä:
(use-package multi-term :ensure t :bind ("C-x M-m" . multi-term))
Näppäränä ominaisuutena multi-term tarjoaa myös dedikoidun bufferin, joka ilmestyy pienikokoisena Emacs-ikkunan alalaitaan komennolla M-x multi-term-dedicated-open. Tämän bufferin voi kääntää päälle ja pois komennolla M-x multi-term-dedicated-toggle. Siihen voi myös loikata komennolla M-x multi-term-dedicated-select, ja sen sulkeminen onnistuu komennolla M-x multi-term-dedicated-close. Tämä bufferi tarjoaa kätevän tavan pitää terminaalia esillä koodatessa samaan tapaan kuin esimerkiksi Visual Studio Codessa.
Lisätään myös näitä komentoja varten pikanäppäimet:
(use-package multi-term :ensure t :bind (("C-x M-m" . multi-term) ("C-x M-o" . multi-term-dedicated-open) ("C-x M-t" . multi-term-dedicated-toggle) ("C-x M-s" . multi-term-dedicated-select) ("C-x M-c" . multi-term-dedicated-close)))

Char-moodin ja line-moodin välillä vaihtaminen onnistuu myös multi-termissä. Siinäkin on siis helppo hypätä esimerkiksi käsittelemään terminaalin tulostetta Emacs-komennoin. Lisätään vielä näiden moodien vaihtamisen välille vastaavat pikanäppäinasetukset kuin term-komennon kanssa:
(use-package multi-term :ensure t :bind (("C-x M-m" . multi-term) ("C-x M-o" . multi-term-dedicated-open) ("C-x M-t" . multi-term-dedicated-toggle) ("C-x M-s" . multi-term-dedicated-select) ("C-x M-c" . multi-term-dedicated-close)) :config (setq term-bind-key-alist (append term-bind-key-alist '(("C-c C-j" . term-line-mode) ("C-c C-k" . term-char-mode)))))
Koska multi-termin terminaalipikanäppäinmäärittelyihin sisäisesti käyttämä term-bind-key-alist-lista ei ole varsinainen keymap, emme voi käyttää use-packagen :bind- ja :map-avainsanoja näppäinasetusten asettamiseen. Sen sijaan joudumme käyttämään :config-avainsanaa ja asettamaan listan arvot manuaalisesti setq-komennolla.
Funktiolla append luomme listan, jossa ovat multi-termin valmiiksi määrittelemät term-bind-key-alistin sisältämät näppäinasetukset sekä omat määrittelemämme näppäinasetukset. Funktio siis lisää kaksi listaa yhteen ja palauttaa uuden listan. Tämä puolestaan asetetaan setq-komennolla term-bind-key-alistin uudeksi arvoksi.
Koska multi-termiä käyttäessä tulee tavallisesti availtua useampia terminaali-ikkunoita, tehdään näiden välillä hyppimisestä helppoa. Sidotaan multi-term-prev- ja multi-term-next-komennot näppäinyhdistemiin C-< ja C->:
(use-package multi-term :ensure t :bind (("C-x M-m" . multi-term) ("C-x M-o" . multi-term-dedicated-open) ("C-x M-t" . multi-term-dedicated-toggle) ("C-x M-s" . multi-term-dedicated-select) ("C-x M-c" . multi-term-dedicated-close)) :config (setq term-bind-key-alist (append term-bind-key-alist '(("C-c C-j" . term-line-mode) ("C-c C-k" . term-char-mode) ("C-<" . multi-term-prev) ("C->" . multi-term-next)))))
Multi-termin voi asettaa myös käyttämään toista terminaalia kuin Bashia. Lisäämme esimerkin vuoksi koodinpätkän, joka ottaa käyttöön Zsh-terminaalin, jos sellainen on saatavilla:
;; Change default multi-term shell to zsh if available (let ((zsh-bin (executable-find "zsh"))) (when zsh-bin (setq multi-term-program zsh-bin)))
Tämä koodinpätkä siis luo let-lausekkeella uuden muuttujan zsh-bin, jonka arvo on executable-find-funktiokutsun tulos. Jos zsh-ohjelmaa ei ole saatavilla, zsh-bin saa arvon nil, jolloin let-lausekkeen body-osassa oleva setq-komento jätetään suorittamatta. Jos taas zsh-ohjelma löytyy, sen zsh-bin-muuttujaan talteen laitettu polku asetetaan multi-term-program-muuttujan arvoksi. Voit vaihtaa zsh:n tilalle minkä tahansa mieleisesi terminaalin.

Yllä oleva koodinpätkä osana use-package-konfiguraatiota:
(use-package multi-term :ensure t :bind (("C-x M-m" . multi-term) ("C-x M-o" . multi-term-dedicated-open) ("C-x M-t" . multi-term-dedicated-toggle) ("C-x M-s" . multi-term-dedicated-select) ("C-x M-c" . multi-term-dedicated-close)) :config ;; Change default multi-term shell to zsh if available (let ((zsh-bin (executable-find "zsh"))) (when zsh-bin (setq multi-term-program zsh-bin))) (setq term-bind-key-alist (append term-bind-key-alist '(("C-c C-j" . term-line-mode) ("C-c C-k" . term-char-mode) ("C-<" . multi-term-prev) ("C->" . multi-term-next)))))
Määritellään vielä lopuksi uusi näppäinkombinaatio, jolla voimme lähettää suoraan terminaaliprosessille C-x-näppäinyhdistelmän. Tällähän hetkellä Emacs ymmärtää C-x:n omaksi komentoetuliitteekseen. Siksi esimerkiksi nano-editorista poistuminen, mikä vaatii C-x-yhdistelmän painalluksen, ei onnistu sulkematta terminaalia. Sidotaan tämän kombinaation lähettäminen terminaalille yhdistelmään C-c C-x.
Koska Emacsin sisäänrakennetusta term.elistä ei löydy valmiiksi komentoa C-x-yhdistelmän lähettämiseksi terminaaliprosessille, sitä varten on määriteltävä oma funktio:
(defun term-send-C-x () (interactive) (term-send-raw-string "\C-x"))
Funktio käyttää term.elissä määriteltyä komentoa term-send-raw-string lähettääkseen näppäinyhdistelmän C-x terminaaliprosessille. \C- on merkkijonomuotoinen tapa esittää näppäinetuliite C-.
Määrittelyllä interactive teemme lisäksi funktiostamme komennon, jota voi käyttää M-x näppäinyhdistelmän takaa, toisin sanoen M-x term-send-C-x. Tämä vaaditaan, koska multi-term odottaa term-bind-key-alistin avain-arvo-parien arvojen olevan pelkkien funktioiden sijaan komentoja.
Hyödynnetään nyt funktiota osana use-package-asetuksiamme:
(use-package multi-term :ensure t :bind (("C-x M-m" . multi-term) ("C-x M-o" . multi-term-dedicated-open) ("C-x M-t" . multi-term-dedicated-toggle) ("C-x M-s" . multi-term-dedicated-select) ("C-x M-c" . multi-term-dedicated-close)) :config ;; Change default multi-term shell to zsh if available (let ((zsh-bin (executable-find "zsh"))) (when zsh-bin (setq multi-term-program zsh-bin))) (declare-function term-send-raw-string "term") (defun term-send-C-x () (interactive) (term-send-raw-string "\C-x")) (setq term-bind-key-alist (append term-bind-key-alist '(("C-c C-j" . term-line-mode) ("C-c C-k" . term-char-mode) ("C-<" . multi-term-prev) ("C->" . multi-term-next) ("C-c C-x" . term-send-C-x)))))
Yllä olevassa päivitetyssä koodissa tapahtuu parikin asiaa. Määrittelemme funktion term-send-C-x ennen term-bind-key-alistin täydentämistä. Kyseisen funktion puolestaan asetamme callback-funktioksi, jota kutsutaan, kun käyttäjä näppäilee näppäinyhdistelmän C-c C-x.
Lisäksi on hyvä huomata, että use-packagen alle on lisätty myös declare-function-määrittely. Kerromme näin Emacs-tavukääntäjälle, että käytämme koodissamme term.el-tiedostossa sijaitsevaa term-send-raw-string-funktiota. Tämän määrittelyn ansiosta Emacs ei anna varoitusta komennon käytöstä.
Siistimisen paikka
Tässä vaiheessa on hyvä huomata, että use-package tarjoaa myös avainsanat ilmoittamaan tavukääntäjälle muuttujia ja funktioita, jotka löytyvät jostain muusta tiedostosta kuin init.elissä. Nämä avainsanat ovat :defines ja :functions. Avainsanaa :defines hyödyntämällä voimme karsia blogisarjan aiemmassa osassa lisäämiämme globaaleja muuttujamäärittelyjä init.elin alusta. Lisäsimme siis edellisessä osassa nämä määritelmät:
;; inform byte compiler about free variables (eval-when-compile (defvar desktop-save) (defvar desktop-path) (defvar desktop-load-locked-desktop) (defvar desktop-auto-save-timeout) (defvar helm-find-files-map) (defvar helm-read-file-map))
Voimme eliminoida kaksi viimeistä määritystä lisäämälle ne :defines-avainsanan alle helm-konfiguraatiomme yhteyteen:
(use-package helm :ensure t :defines (helm-find-files-map helm-read-file-map) :bind (("M-x" . helm-M-x) ("C-x C-f" . helm-find-files) ("C-x b" . helm-mini) :map helm-map ("<tab>" . helm-execute-persistent-action) ("C-z" . helm-select-action) :map helm-find-files-map ("<DEL>" . helm-ff-delete-char-backward) ("C-<backspace>" . helm-find-files-up-one-level) :map helm-read-file-map ("<DEL>" . helm-ff-delete-char-backward) ("C-<backspace>" . helm-find-files-up-one-level)) :init (helm-mode 1))
Tässä muutoksessa on se etu, että nyt nämä muuttujat ovat määritelty vain helmin use-package-konfiguraation näkyvyysalueessa. Tästä herää myös kysymys, voisimmeko käyttää myös multi-term-konfiguraatiossamme declare-function-määrittelyn sijaan :functions-avainsanaa. Funktio term-send-raw-string sijaitsee kuitenkin term.elissä, jota ei kyseisellä rivillä ole ladattu, joten tieto sijainnista pitää välittää erikseen, mitä :functions-avainsana ei tee.
Tämänkertaisten muutosten jälkeen koko init.elimme näyttää siis tältä:
;;; init.el --- Initialization file for Emacs ;;; Commentary: ;; Emacs Startup File (require 'package) ;;; Code: (add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/")) (unless package--initialized (package-initialize)) ;; inform byte compiler about free variables (eval-when-compile (defvar desktop-save) (defvar desktop-path) (defvar desktop-load-locked-desktop) (defvar desktop-auto-save-timeout)) ;; desktop-save settings (desktop-save-mode 1) (setq desktop-save t desktop-path '("~/.emacs.d/") desktop-load-locked-desktop t desktop-auto-save-timeout 5) ;; miscellaneous settings (add-to-list 'default-frame-alist '(fullscreen . maximized)) (add-hook 'before-save-hook 'delete-trailing-whitespace) (global-auto-revert-mode t) (setq column-number-mode t) (setq-default indent-tabs-mode nil) ;; automatic package installation (unless (package-installed-p 'use-package) (package-refresh-contents) (package-install 'use-package)) ;; auto updates (use-package auto-package-update :ensure t :config (setq auto-package-update-delete-old-versions t auto-package-update-interval 4) (auto-package-update-maybe)) ;; external package configurations (use-package helm :ensure t :defines (helm-find-files-map helm-read-file-map) :bind (("M-x" . helm-M-x) ("C-x C-f" . helm-find-files) ("C-x b" . helm-mini) :map helm-map ("<tab>" . helm-execute-persistent-action) ("C-z" . helm-select-action) :map helm-find-files-map ("<DEL>" . helm-ff-delete-char-backward) ("C-<backspace>" . helm-find-files-up-one-level) :map helm-read-file-map ("<DEL>" . helm-ff-delete-char-backward) ("C-<backspace>" . helm-find-files-up-one-level)) :init (helm-mode 1)) (use-package magit :ensure t :bind (("C-x g" . magit-status))) (use-package elpy :ensure t :bind (:map elpy-mode-map ("C-<up>" . nil) ("C-<down>" . nil)) :hook (elpy-mode . (lambda () (add-hook 'before-save-hook 'elpy-format-code nil 'local))) :init (elpy-enable) :config (setq elpy-modules (delq 'elpy-module-flymake elpy-modules))) (use-package flycheck :ensure t :config (global-flycheck-mode)) (use-package multi-term :ensure t :bind (("C-x M-m" . multi-term) ("C-x M-o" . multi-term-dedicated-open) ("C-x M-t" . multi-term-dedicated-toggle) ("C-x M-s" . multi-term-dedicated-select) ("C-x M-c" . multi-term-dedicated-close)) :config ;; Change default multi-term shell to zsh if available (let ((zsh-bin (executable-find "zsh"))) (when zsh-bin (setq multi-term-program zsh-bin))) (declare-function term-send-raw-string "term") (defun term-send-C-x () (interactive) (term-send-raw-string "\C-x")) (setq term-bind-key-alist (append term-bind-key-alist '(("C-c C-j" . term-line-mode) ("C-c C-k" . term-char-mode) ("C-<" . multi-term-prev) ("C->" . multi-term-next) ("C-c C-x" . term-send-C-x))))) (custom-set-variables ;; custom-set-variables was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. '(custom-enabled-themes '(misterioso)) '(elpy-rpc-python-command "python3") '(package-selected-packages '(flycheck elpy magit helm auto-package-update use-package)) '(python-shell-interpreter "python3")) (custom-set-faces ;; custom-set-faces was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. ) (provide 'init) ;;; init.el ends here
Ensi blogisarjan osassa tutkimme puolestaan, miten Emacsista muovataan Python-IDE:n ohella Typescript-editori. Näin Emacsin rajaton monipuolisuus alkaa vähitellen hahmottua.