I closed the terminal that had the kak server, now the clients are sad :(

I like working with multiple Kakoune windows and to do so I launch kak on the first terminal and then open more windows with the :new command. But I have trouble when I close windows. After a while I lose track of which terminal window is the server and which terminal windows are the clients. If I close the server window (click the X button), it kills the server. The clients become sad and I can no longer use :new, for example.

Is there something I can do to avoid the problem?

1 Like

Launch a daemonized server (kak -d -s name) to which you connect afterwards (kak -c name). Not a lot more on this in :h commands and man kak.

Get in the habit of closing a client with :q instead, so kakoune has a chance to complain.

Disown the server from the shell you started it in, for example start the first kakoune via kak & disown.

If you close the Kakoune client with :q the terminal will disappear (if it was a client) or you’ll get back to the shell prompt (if it was a server). Once you quit the “original” instance, Kakoune moves its server to the background so all the existing clients keep running.

The downside is that now we have to use a named session. Perhaps there are clever workarounds with shell aliases; do any of you do this?

This didn’t work for me. The screen bugs out and kakoune becomes unusable.

I’m already using :q, but quitting the main session just returns to the main terminal prompt. Is there a way to make it complain instead?

Ah, but if you quit a running server with :q, there’s nothing for it to complain about - the server just gets backgrounded, instead of dying and breaking all the other clients in the session:

$ kak somefile.txt
Kakoune forked server to background (1987590), for session '1987293'

I wanted to understand the client/server behavior better to see where I misunderstood what disowning does in this context and stumbled across the following:

  • I started a server with kak -d -s hi & disown
  • closed the shell with <c-d> which, to my surprise didn’t also close the terminal application
  • I connected to the server in a new terminal window
  • and tried to create a new client with :new from within kakoune

This fails with

shell stderr: <<<
Error: open /dev/tty: no such device or address

in the debug buffer. Everything else still seems to work.

It seems the server daemonizes but “still has some connection to the terminal”. Why is that? What would be needed, to make different clients, or rather the terminals in which they run, truly interchangeable?

If you haven’t seen it already, there’s a lot of great info on the wiki for client server stuff

When you launch a process in shell in a terminal, it probably has (at least) the following connections to its environment:

  • the shell waits for the process to exit before printing the shell prompt again
  • stdin, stdout, stderr, connected to the terminal device
  • registered in the interactive shell’s list of running jobs
  • its controlling terminal is set to the controlling terminal of the shell

The shell’s & operator causes the shell to not wait for the process to exit before printing the prompt.

The shell’s disown command will remove the process from the shell’s list of jobs.

Redirecting stdin, stdout, and stderr from/to /dev/null will prevent the process from being tied to the terminal in that way.

You may also need a tool like Linux’s setsid command to prevent the process from inheriting the controlling terminal of the shell.

All this is very rarely necessary in practice. If you :q from the original Kakoune instance while clients are connected, Kakoune properly severs the connection from the terminal to the server when it puts the server into the background. If you use kak -d to make a permanent headless session, you can run it from an actual service manager like systemd or runit that handles all the paperwork for you.

Sure, it doesn’t immediately kill the server. However, I still end up with a terminal window that I am not allowed to close because then it would kill the server. And my original goal was to close the window because I had too many windows open…

Is there a way to have “:q” pop up a prompt asking if I really want to close the important server window?

At the moment it seems I might have to bite the bullet and go with a more heavyweight custom script to handle lauching kakoune as a background daemon. I’m a bit apprehensive about leaving a background process even when I’m not editing though. Is there a way to automatically close the background process after the last client exits or would that be too much trouble?

I imagine that’s possible with some kind of hook. There’s a hook for when a client is about to close. At that point you could check the client count and conditionally shutdown the server, I believe the list of client names is available as a variable.

I’m practice I’ve never found myself needing it, so I’ve never made the attempt

What I am seeing there is most certainly a problem in the kitty windowing runtime script, or my kitty configuration: When I start the headless session from a kitty instance, I can’t close the terminal I started from, without having that :new fails with the above error, but when I start the session in an xterm, there is no problem (the X11 variant of windowing steps in, which means I’ll get a new kitty os-window, instead of a new kitty window).

However, I don’t really understand why it is breaking, and I don’t really mind to never close the starting instance, so…

Should probably still figure out whether the issue is with my configuration and fix the bug, if not.

I found a workflow that I like!

Instead of starting out with regular kakoune (kak myfile.txt), launch a headless session on the background (kak -d -s name &), and connect to that with kak -c.

When I do it this way, my shell will ask for confirmation if I try to close the terminal window while the background process is still running.

Note the wiki link by schickm if you want to have a simple wrapper that automates that to one such session per project.

The way kak -d <name> behaves, it should not matter, whether you close the terminal window or not. You should have a running kakoune session that you can connect to with kak -c <name> from any terminal, no matter whether the original terminal window still exists or not. Also, you should be able to get new clients by doing kak -c <name> from different terminals or by doing :new from within any connected client.

If you’re seeing a different behavior, something other than kak -d is causing it.

One note about that wiki page for MacOS users:

The script uses a unix utility setsid which isn’t available on a Mac. Google pointed me to brew install util-linux, however it still wasn’t available in my shell after installing it. I ended up just cloning and building this little project and that worked:

Kakoune already has some logic to fork a server to the background on <c-z> in the client + server process. I think we should also detect SIGHUP and do the same. Could you open an issue ?

@hugomg here is a quick and dirty fix (apply with git am).

From c27e41f19dbd8ad9127fef9789a682ff0f3401de Mon Sep 17 00:00:00 2001
From: Johannes Altmanninger <aclopte@gmail.com>
Date: Sun, 13 Aug 2023 16:35:19 +0200
Subject: [PATCH 1/2] ctrl-z to trigger daemonization also if there is only one

Fixes https://github.com/mawww/kakoune/issues/4957
 src/main.cc | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/src/main.cc b/src/main.cc
index a6762a2d5..bd7b727c9 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -675,11 +675,7 @@ std::unique_ptr<UserInterface> create_local_ui(UIType ui_type)
             set_signal_handler(SIGTSTP, [](int) {
-                if (ClientManager::instance().count() == 1 and
-                    *ClientManager::instance().begin() == local_client)
-                    TerminalUI::instance().suspend();
-                else
-                    convert_to_client_pending = true;
+                convert_to_client_pending = true;
@@ -892,8 +888,10 @@ int run_server(StringView session, StringView server_init,
                 show_startup_info(local_client, global_scope.options()["startup_info_version"].get<int>());
+        bool converting_to_client = false;
         while (not terminate and
-               (not client_manager.empty() or server.negotiating() or server.is_daemon()))
+               (not client_manager.empty() or server.negotiating()
+                    or server.is_daemon() or converting_to_client))
@@ -910,6 +908,8 @@ int run_server(StringView session, StringView server_init,
             catch (const cancel&) {}
+            if (not client_manager.empty())
+                converting_to_client = false;
@@ -946,6 +946,8 @@ int run_server(StringView session, StringView server_init,
                     throw convert_to_client_mode{ std::move(session), std::move(client_name), std::move(buffer_name), std::move(selections) };
+                converting_to_client = true;

From 7aae265dd5cbcd7986891268529147ca0db222e1 Mon Sep 17 00:00:00 2001
From: Johannes Altmanninger <aclopte@gmail.com>
Date: Mon, 14 Aug 2023 22:55:02 +0200
Subject: [PATCH 2/2] Daemonize upon receiving SIGHUP

Fixes https://discuss.kakoune.com/t/i-closed-the-terminal-that-had-the-kak-server-now-the-clients-are-sad/2269/13
 src/main.cc        |  8 +++++++-
 src/terminal_ui.cc | 13 ++++++++-----
 src/terminal_ui.hh |  3 +++
 3 files changed, 18 insertions(+), 6 deletions(-)

diff --git a/src/main.cc b/src/main.cc
index bd7b727c9..a50c6a763 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -601,6 +601,7 @@ void register_options()
 static Client* local_client = nullptr;
 static int local_client_exit = 0;
 static bool convert_to_client_pending = false;
+static bool converting_to_client = false;
 enum class UIType
@@ -677,12 +678,18 @@ std::unique_ptr<UserInterface> create_local_ui(UIType ui_type)
             set_signal_handler(SIGTSTP, [](int) {
                 convert_to_client_pending = true;
+            set_signal_handler(SIGHUP, [](int) {
+                stdin_closed = true;
+                convert_to_client_pending = true;
+                hup = true;
+           });
         ~LocalUI() override
             local_client = nullptr;
             if (convert_to_client_pending or
+                converting_to_client or
@@ -888,7 +895,6 @@ int run_server(StringView session, StringView server_init,
                 show_startup_info(local_client, global_scope.options()["startup_info_version"].get<int>());
-        bool converting_to_client = false;
         while (not terminate and
                (not client_manager.empty() or server.negotiating()
                     or server.is_daemon() or converting_to_client))
diff --git a/src/terminal_ui.cc b/src/terminal_ui.cc
index f87a914e9..e28ed1d27 100644
--- a/src/terminal_ui.cc
+++ b/src/terminal_ui.cc
@@ -429,7 +429,8 @@ static constexpr StringView assistant_dilbert[] =
 template<typename T> T sq(T x) { return x * x; }
 static sig_atomic_t resize_pending = 0;
-static sig_atomic_t stdin_closed = 0;
+sig_atomic_t stdin_closed = 0;
+sig_atomic_t hup = 0;
 template<sig_atomic_t* signal_flag>
 static void signal_handler(int)
@@ -474,9 +475,12 @@ TerminalUI::TerminalUI()
-    enable_mouse(false);
-    restore_terminal();
-    tcsetattr(STDIN_FILENO, TCSAFLUSH, &m_original_termios);
+    if (not hup)
+    {
+        enable_mouse(false);
+        restore_terminal();
+        tcsetattr(STDIN_FILENO, TCSAFLUSH, &m_original_termios);
+    }
     set_signal_handler(SIGWINCH, SIG_DFL);
     set_signal_handler(SIGHUP, SIG_DFL);
     set_signal_handler(SIGTSTP, SIG_DFL);
@@ -669,7 +673,6 @@ Optional<Key> TerminalUI::get_next_key()
     if (stdin_closed)
         set_signal_handler(SIGWINCH, SIG_DFL);
-        set_signal_handler(SIGHUP, SIG_DFL);
         if (m_window)
diff --git a/src/terminal_ui.hh b/src/terminal_ui.hh
index dd4ef870b..3b4a6f4f4 100644
--- a/src/terminal_ui.hh
+++ b/src/terminal_ui.hh
@@ -19,6 +19,9 @@ namespace Kakoune
 struct DisplayAtom;
 struct Writer;
+extern sig_atomic_t stdin_closed;
+extern sig_atomic_t hup;
 class TerminalUI : public UserInterface, public Singleton<TerminalUI>

Hi Krobelus, thanks for the patch! But unfortunately I couldn’t get it to work :(. I opened an issue on Github (issue #4960)

@hugomg This

  • Launch kak on a terminal
  • Open a client with :new
  • Close the original kak
  • Try to run :new from the client.

should not need a modification to kakoune to get working.
The session is daemonized as soon as :q-ing the first client, so the second client will stay alive and able to :new other clients.

What kakoune version, window manager and terminal emulator are you using? What does :terminal turn out to be aliased to?

1 Like

For all following this thread, the issue that @hugomg opened to fix the automatic daemonizing on backgrounding has been fixed in the development branch:

For this those that run of the current state of master, you should be good to go now!

Because Kakoune does the automatic backgrounding and daemonizing…do we really need to have this complex script on the wiki that is manually handling the backgrounding and daemonizing?


server_name=$(basename `PWD`)
socket_file=$(kak -l | grep $server_name)

if [[ $socket_file == "" ]]; then        
    # Create new kakoune daemon for current dir
    setsid kak -d -s $server_name &

# and run kakoune (with any arguments passed to the script)
kak -c $server_name $@

I think it could just be trimmed down to something like…


server_name=$(basename `PWD`)

if ! [[ kak -l | grep $server_name ]]; then        
    kak -d -s $server_name & disown

kak -c $server_name $@

That feels easier to understand to me and lets kakoune handle the socket file.

Thoughts anyone?

1 Like