From 1d0a15e91596a8193d765639b547c460e7abbf64 Mon Sep 17 00:00:00 2001 From: Axel Fontaine Date: Mon, 17 Aug 2026 13:34:17 +0200 Subject: [PATCH] Hot-reload the active skin when its file changes on disk Currently, picking up a change to the current skin's .ini file requires restarting mc, or re-selecting the same skin from Options > Appearance. This is inconvenient for skins that are regenerated by an external process (e.g. a script that derives an mc skin from the desktop theme and wants it to apply live, without the user closing and reopening mc). On Linux, watch the active skin file with inotify and re-apply it automatically on change, reusing the same reload path (mc_skin_deinit + mc_skin_init + panel/filehighlight refresh + repaint_screen) already used by the interactive skin picker in the Appearance dialog, now exposed as mc_skin_reload(). The watch fd is integrated into the existing select() loop via add_select_channel()/delete_select_channel(), the same generic mechanism already used for background job and subshell pipes. The watch is armed once at startup and intentionally never re-armed on each event: our write pattern is an in-place truncate+write, which never invalidates the original watch descriptor. Re-arming from within the event callback was tried first and found to self-trigger forever, since inotify_rm_watch() itself raises IN_IGNORED on the same fd, which the next select() sees immediately. This is Linux-only (inotify has no equivalent on the other platforms mc supports), guarded with #ifdef __linux__, matching existing precedent elsewhere in the tree (cons.handler.c, key.c, subshell/proxyfunc.c). Signed-off-by: Axel Fontaine --- src/filemanager/boxes.c | 10 +++++ src/filemanager/boxes.h | 1 + src/main.c | 85 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/src/filemanager/boxes.c b/src/filemanager/boxes.c index be09c241fe..aa48c4f3f3 100644 --- a/src/filemanager/boxes.c +++ b/src/filemanager/boxes.c @@ -485,6 +485,16 @@ task_cb (WButton *button, int action) /*** public functions ****************************************************************************/ /* --------------------------------------------------------------------------------------------- */ +/* Re-read the currently configured skin from disk and redraw. Used to hot-reload + the skin file when it changes on disk (see the skin_watch_* functions in main.c). */ +void +mc_skin_reload (void) +{ + skin_apply (NULL); +} + +/* --------------------------------------------------------------------------------------------- */ + void about_box (void) { diff --git a/src/filemanager/boxes.h b/src/filemanager/boxes.h index 21e031e56a..212694ef4b 100644 --- a/src/filemanager/boxes.h +++ b/src/filemanager/boxes.h @@ -18,6 +18,7 @@ /*** declarations of public functions ************************************************************/ +void mc_skin_reload (void); void about_box (void); void configure_box (void); void appearance_box (void); diff --git a/src/main.c b/src/main.c index a67270150e..2ae7badfc2 100644 --- a/src/main.c +++ b/src/main.c @@ -31,6 +31,7 @@ #include +#include #include #include // for username in xterm title #include @@ -40,6 +41,9 @@ #include #include #include // getsid() +#ifdef __linux__ +#include // skin hot-reload watch +#endif #include "lib/global.h" @@ -60,6 +64,7 @@ #include "filemanager/ext.h" // flush_extension_file() #include "filemanager/command.h" // cmdline #include "filemanager/panel.h" // panalized_panel +#include "filemanager/boxes.h" // mc_skin_reload() #ifdef USE_INTERNAL_EDIT #include "editor/edit.h" // edit_arg_free() @@ -90,10 +95,82 @@ /*** file scope variables ************************************************************************/ +#ifdef __linux__ +// Hot-reload: watch the active skin file and re-apply it on change. +static int skin_watch_fd = -1; +#endif + /* --------------------------------------------------------------------------------------------- */ /*** file scope functions ************************************************************************/ /* --------------------------------------------------------------------------------------------- */ +#ifdef __linux__ + +/* Arm the watch on whatever skin file is currently loaded. Only called once, + at startup: our write pattern (in-place truncate+write, not rename) never + invalidates the watch descriptor, so there is nothing to re-arm on reload. + (Calling inotify_rm_watch() from the event callback would itself raise + IN_IGNORED on the same fd, which the next select() sees immediately -- + an infinite self-triggering loop.) */ +static void +skin_watch_arm (void) +{ + if (mc_skin__default.config == NULL || mc_skin__default.config->ini_path == NULL) + return; + + inotify_add_watch (skin_watch_fd, mc_skin__default.config->ini_path, + IN_MODIFY | IN_CLOSE_WRITE); +} + +/* --------------------------------------------------------------------------------------------- */ + +static int +skin_watch_callback (int fd, void *info) +{ + char buf[4096] __attribute__ ((aligned (__alignof__ (struct inotify_event)))); + + (void) info; + + // Drain all pending events before acting; a plain "w" rewrite of the skin + // file typically raises both IN_MODIFY and IN_CLOSE_WRITE for one save. + while (read (fd, buf, sizeof (buf)) > 0) + ; + + mc_skin_reload (); + + return 0; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +skin_watch_init (void) +{ + skin_watch_fd = inotify_init1 (IN_NONBLOCK | IN_CLOEXEC); + if (skin_watch_fd < 0) + return; + + skin_watch_arm (); + add_select_channel (skin_watch_fd, skin_watch_callback, NULL); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +skin_watch_deinit (void) +{ + if (skin_watch_fd < 0) + return; + + delete_select_channel (skin_watch_fd); + close (skin_watch_fd); + skin_watch_fd = -1; +} + +#endif // __linux__ + +/* --------------------------------------------------------------------------------------------- */ + /** POSIX version. The only version we support. */ static void OS_Setup (void) @@ -372,6 +449,10 @@ main (int argc, char *argv[]) mc_error_message (&mcerror, NULL); +#ifdef __linux__ + skin_watch_init (); +#endif + #ifdef ENABLE_SUBSHELL // Done here to ensure that the subshell doesn't // inherit the file descriptors opened below, etc @@ -450,6 +531,10 @@ main (int argc, char *argv[]) flush_extension_file (); // does only free memory +#ifdef __linux__ + skin_watch_deinit (); +#endif + mc_skin_deinit (); tty_colors_done ();