BIRKEY CONSULTING

ABOUT  RSS  ARCHIVE


Posts tagged "emacs":

18 Jun 2023

Google Bard and Emacs

After reading a Google blog post on Bard's increasing ability for reasoning about source code, I thought I would give it a try. The issue is that not like OpenAI, Bard currently does not have an http API that I can use via curl. I googled around and came across the `bard-rs` project here: https://github.com/Alfex4936/Bard-rs. So I followed the excellent instruction to get set up using bard from command line and its is pretty solid. I used following Elisp to use `bard-rs` from Emacs' compilation buffer here:

(defun kcompilation-start (cmd name &optional mode)
  (let* ((compile-command nil)
         (compilation-save-buffers-predicate 'ignore)
         (compilation-buffer
          (compilation-start cmd
                             (if (equal mode 'read-only) nil t)
                             (lambda (m)
                               (or (when (boundp 'name)
                                     (format "*%s*" name))
                                   (buffer-name))))))
    (when current-prefix-arg
      (with-current-buffer compilation-buffer
        (switch-to-prev-buffer (get-buffer-window (current-buffer)))))
    (message (format "Running %s in %s ..." cmd name))))

(defun kprompt-bard (&optional p)
  "Prompts for input to send it to `bard` using `bard-rs` in
*bard-prompt* buffer. If mark-active, uses the text in the region
 as the prompt"
  (interactive "P")
  (let* ((bs "bard-prompt")
         (bname (format "*%s*" bs))
         (bname (if (get-buffer bname)
                    bname
                  (progn (kcompilation-start "bard-rs -e ~/.env" bs)
                         bname)))
         (prompt (if mark-active
                     (replace-regexp-in-string
                      "\n"
                      ""
                      (buffer-substring-no-properties (region-beginning) (region-end)))
                   (read-string "AI Chat Prompt: "))))
    (with-current-buffer (pop-to-buffer bname)
      (when p
        (end-of-buffer)
        (insert "!reset")
        (comint-send-input)
        (end-of-buffer)
        (insert prompt)
        (comint-send-input))
      (when (not p)
        (end-of-buffer)
        (insert prompt)
        (comint-send-input)))))

You can bind `kprompt-bard` to any key of your choice and start interacting with Google bard from the comfort of Emacs' buffer.

Tags: AI emacs
14 May 2023

AI or not to AI

1913 Webster dictionary gives following definition to Artificial Intelligence: Artificial - 1. Made or contrived by art; produced or modified by human skill and labor, in opposition to natural; 2. Feigned; fictitious; assumed; affected; not genuine. 3. Artful; cunning; crafty. 4. Cultivated; not indigenous; not of spontaneous growth; Intelligence - 1. The act or state of knowing; the exercise of the understanding. 2. The capacity to know or understand; readiness of comprehension; the intellect, as a gift or an endowment. 3. Knowledge imparted or acquired, whether by study, research, or experience; general information. Specifically; (Mil.) Information about an enemy or potential enemy, his capacities, and intentions.

Let us read and re-read above definitions and give it a few minutes to sink in. With our skilled labor, we have managed to produce a very powerful fictitious software that can understand, produce and reason about human generated artifacts such as language, images and videos. Our ability to make it more scalable, more accurate and more controllable is just a matter of time considering what we have is already pretty powerful enough. My following interaction with ChatGPT 3.5 is already impressive:

[ME]: Explain, then add tests and simplify if possible:
(defun kprompt (&optional p)
  (interactive "P")
  (let* ((bname (if p "*org-ai-on-region*" "*scratch*"))
         (directive "Explain, then add tests and simplify if possible:\n")
         (prompt (if p
                     (buffer-substring (region-beginning) (region-end))
                   (read-string "AI Chat Prompt: "))))
    (with-current-buffer (pop-to-buffer bname)
      (when p
        (erase-buffer)
        (org-mode)
        (setq truncate-lines nil)
        (setq word-wrap t)
        (org-ai--insert-created-timestamp)
        (insert "\n")
        (insert "#+begin_ai\n" "[ME]: " directive prompt "\n" "#+end_ai\n")
        (forward-line -1)
        (org-ai-complete-block))
      (when (not p)
        (beginning-of-buffer)
        (open-line 2)
        (org-ai-prompt prompt)))))

[AI]: This function takes an optional argument, p, which determines whether the user
will be prompted to enter a text or whether the function will use the text highlighted
in the buffer. 

If p is provided, it will create a new buffer with the name "*org-ai-on-region*" and
insert the highlighted text from the current buffer along with a directive. This new
buffer buffer will be formatted using org-mode to allow for better organization of
comprehension text. Finally, it will call org-ai-complete-block to add some
AI-generated text to the prompt.

If p is not provided, it will prompt the user to enter text and then call
org-ai-prompt org-ai-prompt to add some AI-generated text to the prompt.

Tests:

Input:
(kprompt)
Expected Output:
AI Chat Prompt: 

Input:
(kprompt t)
Expected Output:
*org-ai-on-region* buffer created with prompt

Input:
(Highlight "This is a test")
(kprompt t)
Expected output:
*org-ai-on-region* buffer created with "This is a test" text and AI-generated text.

Simplified Code:
The code appears to be relatively straightforward and efficient.
No simplification is needed.

It understood and reasoned about a piece of code (simple for now but does not matter as it can get pretty powerful in no time) despite the fact that it has never seen above code before in its entirety (I am pretty sure about that since it is a private code and this is the only time I am making the code public). This degree of intelligence codified into a software that a few powerful cooperation currently dominates should be something that keeps every software engineers awake at night. Not because of its inherent danger or tremendous productivity boosting ability as folks on the opposite side of spectrum of current AI debate claimed, but because of the very fact that every aspect of human lives will be effected by a such a powerful code like ChatGPT whether we like it or not, and we need to do whatever we can to ensure it is used for the good of humanity in general. It is created by humans and should serve humans. Make no mistake about it. Powerful software systems like that is already used by big cooperations and rouge states to cajole people into a state of self censorship if not into a state of heedlessness of its future implication. Social media, powerful tracking and image recognition systems are already pervasive in the lives of millions of people that are being controlled by dictators all around the world (and it is being exported very actively in the name of economic progress) to socially engineer people's behaviors that benefits their agenda in the name of social and economic progress at the very expense of destroying anyone or anything that is deemed as an obstacle.

As a software engineer who have seen the worst of what bad actors can do with such a powerful systems, I am calling out to all of my fellow engineers to start thinking about what kind of world we would like our kids to inherit from us regardless of where you are, who you are and what is your geopolitical affiliation is. The wave is already there, and it takes all of us to make sure we are not being social engineered out of our humanity. I believe in the power of our humanity to make AI to work for us not the other way around. I registered the domain www.codeforhumanrights.org few years ago and this might be a good time to start putting it to a good use. If you are reading this and feel the need to start doing something, reach out to me via ktuman at acm dot org.

Tags: AI emacs
04 Mar 2023

Atomic commits made easy

Code complexity is something we all deal with in our daily work. There are many tools to helps us manage it. One of the most important one is to make incremental changes where each change is about one and one context alone , which is a great definition of an atomic commit. I do not think I need to convince you about its benefits any further than what I already have alluded to above, which is worth repeating here: It helps us contain complexity within our code base. In pursuit of making it easy for me to do atomic commits, I settled down following workflow:

Having armed with above convention, I incorporated following tools to help me to make atomic commits easy:

I am not going to repeat what the excellent blog talked about above tools here, but it is worth checking it out, and I highly recommend it. If you happen to use Emacs, here is how you add it to your config:

;; clone above repo in to ~/repos and eval following code
(load-file "~/repos/commit-patch/commit-patch-buffer.el")
(eval-after-load 'diff-mode
  '(require 'commit-patch-buffer nil 'noerror))

With above configuration, you can M-x vc-diff a file (vc-root-diff for whole project) then kill, split or edit the resulting hunks using diff mode's built-in commands and to then hit C-c C-c to commit the patch. Later if you realized that your commit is not atomic, you can make further changes and amend previous commit by C-c C-C (note the upper case C).

Tags: vc emacs
01 Aug 2021

Why Eshell? - Part 5

One of the feature of Eshell that took me sometime to really appreciate is its built-in ability to emulate Plan9 smart Shell. It allows you to run a script or a command, run it again by just hitting enter key after you modify it, say you made a mistake or want to change part of it using Emacs editing power. You might say that you can do same thing in any terminal using your up arrow key and command line editing. But I challenge you to try it until you fully realize the advantage you have vs regular terminal. Below is a simple screen recording to show you what I mean:

eshell-plan9-smart-shell.gif

I recommend you read following post to learn more about smart shell and more about eshell including aliases: https://masteringemacs.org/article/complete-guide-mastering-eshell.

This concludes my `Why Eshell` series. Hope you find it useful and happy Eshelling!

Tags: eshell emacs
17 Jul 2021

Why Eshell? - part 4

Since eshell buffer is just a regular emacs buffer, we have all of the emacs power at our disposal. In part 3 of my post, I briefly alluded to multiple terminal management by just using few lines of elisp and all without using any third party packages. I am posting the main elisp function for posterity here:

(defun krun (cmd)
  (interactive
   (list
    (ido-completing-read
     "Enter cmd to run (append ##name for buffer name): "
     (let ((history nil))
       ;; We have to build up a list ourselves from the ring vector.
       (dotimes (index (ring-length keshell-history-global-ring))
	 (push (ring-ref keshell-history-global-ring index) history))
       ;; Show them most-recent-first.
       (setq history (nreverse history))))))
  (let* ((cmds (split-string cmd "##"))
	 (tag (or (-> cmds second)
		  "kshell"))
	 (buff-name (-> tag s-chomp s-trim)))
    (kshell cmd buff-name)))

The above function (along with those from the previous post) will allow you to do following:

Following demonstrates above cases I am talking about:

eshell-buffer-management.gif
Tags: eshell emacs
10 Jul 2021

Why Eshell? - Part 3

If you have been following my `Why Eshell` series, you might be wondering about interactive ido completion for eshell. Since Eshell buffer is just a regular emacs buffer, we can compose few emacs builtin functionalities to bend it to our command line work flow. Following is all the code you need. Note that it is heavily commented so you can understand what is happening.

(defun kshell-with-name (&optional name)
  ;; creates an eshell buffer with `kshell' as the default name. Take
  ;; optional name to override
  (let ((m (->> (or name "kshell")
		(format "*%s*"))))
    (if (get-buffer m)
	(pop-to-buffer m)
      (progn
	(eshell)
	(rename-buffer m)))))

(defun kshell (&optional cmd buff-name send-input)
  ;; interactive fn that you can call via M-x or hotkey. It detects
  ;; current project root to start eshell buffer if no `buff-name' is
  ;; given. You can control how the `cmd` gets insert only or insert
  ;; and executed using `send-input` flag. Ex. Useful for further
  ;; editing.
  (interactive)
  (let ((dir (projectile-project-root))) ;; you need projectile
    (if buff-name
	(kshell-with-name buff-name)
      (kshell-with-name))
    (eshell/clear-scrollback)
    (insert (format "cd %s" (or dir "~/")))
    (eshell-send-input)
    (insert (format "%s" cmd))
    (when send-input (eshell-send-input))))

(defun krun (cmd)
  ;; eshell command history with ido completion. Assign it to a hot
  ;; key say, F12 and you will get a search-able command history that
  ;; you can execute just by doing ido interactive search.
  (interactive
   (list
    (ido-completing-read
     "Enter cmd to run (append ##name for buffer name): "
     (let ((history nil))
       ;; We have to build up a list ourselves from the ring vector.
       (dotimes (index (ring-length keshell-history-global-ring))
	 (push (ring-ref keshell-history-global-ring index) history))
       ;; Show them most-recent-first.
       (setq history (nreverse history))))))
  (let* ((cmds (split-string cmd "##")) ;; you need s.el lib
	 (tag (or (-> cmds second)
		  "kshell"))
	 (buff-name (-> tag s-chomp s-trim)))
    (kshell cmd buff-name)))

Here is the screen recording in action:

eshell-ido-interactive.gif

I am using F12 to invoke `krun` and my cross session eshell history shows up in the minibuffer where I can use ido completion to select a command that I can run. As an added benefit, above code allows one to bring a eshell buffer with specific name by adding ##name at the end of the command if one wants to have the command run in a dedicated buffer.

Tags: eshell emacs
27 Jun 2021

Why Eshell? - Part 2

Among many reasons of why I use eshell as my main terminal, I listed following in part 1 of my `Why Eshell?` series blog post. I am listing it here for posterity:

  1. Long running command notification and time
  2. Cross session history
  3. Interactive ido completion
  4. Unified interface (shell prompt buffer as regular emacs buffer)
  5. Plan9 Style Shell prompt (Think of it as bash REPL)
  6. Multiple terminal management
  7. Super charge bash with elisp
  8. Eshell aliases that puts bash aliases to shame :)

I addressed first point here: notification and time. Let me address second point, which is sharing eshell history across many terminal sessions.

(defvar keshell-history-global-ring nil
  "The history ring shared across Eshell sessions.")

(defun keshell-hist-use-global-history ()
  "Make Eshell history shared across different sessions."
  (unless keshell-history-global-ring
    (when eshell-history-file-name
      (eshell-read-history nil t))
    (setq keshell-history-global-ring
	  (or eshell-history-ring (make-ring eshell-history-size))))
  (setq eshell-history-ring keshell-history-global-ring))
;; Following hook enables it
(add-hook 'eshell-mode-hook 'keshell-hist-use-global-history)
;; Following removes the hook
(remove-hook 'eshell-mode-hook 'keshell-hist-use-global-history)

After evaluating above 12 lines of elisp, you will have a list that holds all the eshell entries across sessions, which you can persist into a history file (just uses bash history file) and can even share it across machines over network. This opens up a whole new possibilities of completions, deduplication and multiple eshell buffer management goodness, which I will cover in the next few parts of `Why Eshell?` posts. So stay tuned and happy eshelling!

Tags: eshell emacs
20 Jun 2021

Why Eshell? - Part 1

From time to time, I got asked why I use eshell as my main terminal. There are multiple reasons I do so and covering all of them in one post would be too long. Instead, I would like to start with a high level bullet points and address each one in a separate blog post with working code examples where I can point my coworkers and friends to them so they can pick and choose as they wish.

  1. Long running command notification and time
  2. Cross session history
  3. Interactive ido completion
  4. Unified interface (shell prompt buffer as regular emacs buffer)
  5. Plan9 Style Shell prompt (Think of it as bash REPL)
  6. Multiple terminal management
  7. Super charge bash with elisp
  8. Eshell aliases that puts bash aliases to shame :)

Let us start with how you can accomplish the first item from the list above. Following 18 lines of elisp will give you the super power of:

;; eshell time and notification
(defvar-local eshell-current-command-start-time nil)

(defun eshell-current-command-start ()
  (setq eshell-current-command-start-time (current-time)))

(defun eshell-current-command-stop ()
  (when eshell-current-command-start-time
    (let ((elapsed-time (float-time
			 (time-subtract (current-time)
					eshell-current-command-start-time))))
      (if (> elapsed-time 30)
	  (tooltip-show (format "Finished in: %.0fs" elapsed-time))
	(eshell-interactive-print
	 (format "Time: %.0fs\n" elapsed-time))))
    (setq eshell-current-command-start-time nil)))

(defun eshell-current-command-time-track ()
  (add-hook 'eshell-pre-command-hook #'eshell-current-command-start nil t)
  (add-hook 'eshell-post-command-hook #'eshell-current-command-stop nil t))

;; This line below installs time tracking and notification
(add-hook 'eshell-mode-hook #'eshell-current-command-time-track)
;; Once you eval above snippet in emacs, fire up M-x eshell, and type:
sleep 40
;; You can switch away from emacs and will be notified that above
;; command took 40s to run
;; To uninstall
;; (remove-hook 'eshell-mode-hook #'eshell-current-command-time-track)

Try doing above with some bashrc voodoo or plug-in that you have no control over. I have been there and done that and it is one of many reasons why I use eshell now. I hope someone finds it useful to his or her command line work flow. Happy eshelling!

Tags: emacs eshell
04 Aug 2019

Why Emacs

I have been thinking about writing up my experience of using Emacs and its positive influence on me as a Software Craftsman over the last 10 years or so. Over the weekend, I came across a a blog post by the CTO of a company, who put it very well that have really resonated with me. It is a great read, not too long not too short, that I strongly recommend you go ahead read and come back here to continue with the rest of what I had to say. Here is the link: Two Years With Emacs as a CEO (and now CTO).

So I am not going to repeat what he said about Emacs and why he still loves to use it to get things done. I am going to add following points on top of what he wrote there:

Emacs is a great workbench

I see Emacs as an extremely well designed workbench that will evolve with you as your work environment, technology and paradigms change. Emacs is a live programming environment where you can change every aspects of its functionalities, be it a simple text editing to a complex work flow where you can interact with many external systems. One of a startup that I worked had a mode where we have interacted with our live system written in Clojure via its CLI interface using `comint-mode`, which you should really checkout if you are not familiar with it. You can use Emacs as the client with uniform interface to many of the CLI, API and even for ABI. For example `EXWM`, which is an Window Manager, is a great example.

Emacs is a great structured and unstructured text manipulation library

This is what I personally like a lot about Emacs. As a programmer, we work with manipulating text all the time. Yes, there are great text Editors out there, which by the way I have used all of them myself before seeing the light of using Emacs. But I see all other editors/IDEs as a framework for the things they set out to do rather than a library that I can compose to solve a particular problem at hand. For example, I can use `smartparens` package for structured text editing of Clojure code.

Emacs helps you focus

In this day of our age where everything tries to grab your constant attention, Emacs stands out as the most distraction free environment to be in. One might argue that Emacs user spends way more time configuration management, which is definitely true when you are just getting started with any new toolbox, I found I spend very little time with configuration or keeping it up to date. Once you are familiar with Emacs help system and built in Documentation, You can be pretty much on your own when it comes to how much you spend on customization. It is this freedom you get from using Emacs where you are in charge as to how much customization you would like. The reason I say Emacs helps you focus is due to the fact that you have the freedom to make it an distraction free writing/coding/communication environment to get done what matters the most to you and have a lot of fun along the way.

Emacs has a learning curve

Yes, I do acknowledge that Emacs has a learning curve. However, most of it is due to the wrong approach we tend to take when encountering an unknown. On hindsight, I wish I had following approach, which I do now all the time:

  1. Start with getting familiar with Emacs terminology such as windows, buffers and frames etc. The builtin documentation is great for that and it is just C-h i (Hit Control and letter `h` at the same time, then hit `i`, which stands for info). To read Emacs' manual just enter `m` and select `Emacs` from minibuffer. Hmm, you might be wondering what is minibuffer, let us use this approach to find out. Press following C-h i m key sequence and type `Minibuffer` and hit enter. Voila, you are reading all about `Minibuffer` from the official Emacs manual. No other application that I have used comes close to the level of Emacs in terms of self documentation.
  2. Start with the goal of getting something done. I have two example that every programmer will benefit from learning: `Magit` and `Org-mode`. While you need to install `magit-mode`, which is a Emacs interface to git CLI, `org-mode` is built in. You will be surprised to find that it makes your git journey so much fun or your note organization so much enjoyable. I made the mistake of trying to memorize hot keys as much as I can before learning the hard truth of learning one key at a time as needed bases. I strongly recommend the only hot key you need at the beginning is `M-x`, where you just type a command so Emacs can execute for you. For Example, if you would like open (it is `visit` in Emacs speak) a file, just type `M-x` and type `file` then press tab key, select `find-file` in the completion buffer, now you can choose a file name from within the directory that you are in. It is just an example but knowing the fact that every action you perform in Emacs invokes an command, which is just an elisp function, is a very powerful realization. That means you can write an elisp function to have Emacs do whatever you like. You can also lookup what a command does using C-h f, then typing the name of the command. For example, C-h f then, type `org-insert-link` then enter and read on…
  3. I advise against starting with Emacs using other's configuration including Emacs that comes with pre-configuration. As I have mentioned earlier, Emacs is a great library where you can pick and choose to fit your work style. You will loose this great aspects if you start with other's way of configuring/composing it. If you had to just start with a minimal config, put this in your `init.el` file and start changing/adding/organizing configs as you see fit. For example:
;; Emacs reads init.el file located in ~/.emacs.d at startup
;; bootstrap el-get
(add-to-list 'load-path "~/.emacs.d/el-get/el-get")
(unless (require 'el-get nil 'noerror)
  (with-current-buffer
      (url-retrieve-synchronously
       "https://raw.githubusercontent.com/dimitri/el-get/master/el-get-install.el")
    (goto-char (point-max))
    (eval-print-last-sexp)))

;; Initialize available packages first
(package-refresh-contents t)

;; The only package you need to get started
(el-get-bundle smex
  (progn
    (require 'smex)
    (global-set-key (kbd "M-x") 'smex)))

Above snippets sets you up to use `El-get`, a great package manager that I came to rely over the years and have never failed me. It also pulls in the only package you had to have before getting started, `smex` that makes the only command `M-x` you need much more intuitive. Then say if you want to try `magit`, just type M-x el-get-install, then `magit`. El-get will download it and install it so you can starting using it in your git projects. Once you can find your way around Emacs, you can start your journey of how best organize your config/customization as you go along. I use just one org file for it and you might or might not like it. Here is the file in its entirety if you are interested: My config snapshot

Tags: emacs
19 Sep 2018

Blogging Made Simple

After taking some time off from work to focus on projects that I like to take on, I set out to find a simple way to use git and emacs workflow to start blogging again. Among plethora of options on the web, I came across this project - Org Static Blog - that I resonated with. It took me no time to set it up and start blogging. Below is my setup:

(use-package org-static-blog
  :straight t
  :config
  ;; -----------------------------------------------------------------------------
  ;; Set up blogging in Emacs
  ;; -----------------------------------------------------------------------------

  (setq org-static-blog-publish-title "BirkeyCo")
  (setq org-static-blog-publish-url "https://www.birkey.co/")
  (setq org-static-blog-publish-directory "~/github/oneness.github.io/")
  (setq org-static-blog-posts-directory "~/github/oneness.github.io/posts/")
  (setq org-static-blog-drafts-directory "~/github/oneness.github.io/drafts/")
  ;;(setq org-static-blog-enable-tags t)
  (setq org-export-with-toc nil)
  (setq org-export-with-section-numbers nil)

  (setq org-static-blog-page-header
	"<meta name=\"author\" content=\"Kasim Tuman\">
<meta name=\"referrer\" content=\"no-referrer\">
<link href= \"static/style.css\" rel=\"stylesheet\" type=\"text/css\" />
<link rel=\"icon\" href=\"static/favicon.ico\">
<link rel=\"apple-touch-icon-precomposed\" href=\"static/birkey_logo.png\">
<link rel=\"msapplication-TitleImage\" href=\"static/birkey_logo.png\">
<link rel=\"msapplication-TitleColor\" href=\"#0141ff\">
<script src=\"static/katex.min.js\"></script>
<script src=\"static/auto-render.min.js\"></script>
<link rel=\"stylesheet\" href=\"static/katex.min.css\">
<script>document.addEventListener(\"DOMContentLoaded\", function() { renderMathInElement(document.body); });</script>
<meta http-equiv=\"content-type\" content=\"application/xhtml+xml; charset=UTF-8\">
<meta name=\"viewport\" content=\"initial-scale=1,width=device-width,minimum-scale=1\">")

  (setq org-static-blog-page-preamble
	"<div class=\"header\">
  <a href=\"https://birkey.co\">Code, Data and Network</a>
  <div class=\"sitelinks\">
    <a href=\"https://twitter.com/KasimTuman\">Twitter</a> | <a href=\"https://github.com/oneness\">Github</a>
  </div>
</div>")

  (setq org-static-blog-page-postamble
	"<div id=\"archive\">
  <a href=\"https://www.birkey.co/archive.html\">Other posts</a>
</div>
<center><a rel=\"license\" href=\"https://creativecommons.org/licenses/by-sa/3.0/\"><img alt=\"Creative Commons License\" style=\"border-width:0\" src=\"https://i.creativecommons.org/l/by-sa/3.0/88x31.png\" /></a><br /><span xmlns:dct=\"https://purl.org/dc/terms/\" href=\"https://purl.org/dc/dcmitype/Text\" property=\"dct:title\" rel=\"dct:type\">birkey.co</span> by <a xmlns:cc=\"https://creativecommons.org/ns#\" href=\"https://www.birkey.co\" property=\"cc:attributionName\" rel=\"cc:attributionURL\">Kasim Tuman</a> is licensed under a <a rel=\"license\" href=\"https://creativecommons.org/licenses/by-sa/3.0/\">Creative Commons Attribution-ShareAlike 3.0 Unported License</a>.</center>")

  ;; This slows down org-publish to a crawl, and it is not needed since
  ;; I use magit anyway.
  (remove-hook 'find-file-hooks 'vc-find-file-hook))
Tags: emacs blogging
Other posts