This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch tmux-integration
in repository terminology.
View the commit online.
commit c3bb3bbba88f33a6b75aa23740c18dd581a86617
Author: Cedric BAIL <[email protected]>
AuthorDate: Mon Mar 30 17:59:49 2026 -0600
feat(macos): add .app bundle, native menu bar, Cocoa integration, and docs
Add macOS-specific support:
- .app bundle generation via meson (Info.plist, launcher binary, icns
generation, symlink-based app structure)
- Native menu bar with New Window, Copy, Paste, Quit via elm_win
main menu API, guarded by __APPLE__ ifdefs
- Cocoa reopen handler to create a new window when the Dock icon is
clicked with no visible windows
- Move window position (evas_object_move) before evas_object_show
so EFL respects the initial placement on macOS
- Add macOS build and install instructions to README
---
README.md | 39 ++++++++++++++
data/macos/Info.plist.in | 39 ++++++++++++++
data/macos/gen_icns.sh | 23 ++++++++
data/macos/launcher.c | 17 ++++++
data/macos/link_app.sh | 21 ++++++++
data/macos/meson.build | 51 ++++++++++++++++++
data/meson.build | 3 ++
meson.build | 5 ++
src/bin/main.c | 64 +++++++++++++++++++----
src/bin/win.c | 133 +++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 385 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index e198f50e..ba0f6098 100644
--- a/README.md
+++ b/README.md
@@ -447,3 +447,42 @@ Then the following environment need to be set:
* ``ELM_DISPLAY=fb``
* ``ELM_ACCEL=none``
* ``EVAS_FB_DEV=/dev/fb0``
+
+## macOS
+
+On macOS, the build system produces a `Terminology.app` bundle that can be
+installed into `/Applications` or run directly from the build directory.
+
+### Building on macOS
+
+NLS (native language support) must be disabled because macOS ships a
+`libintl` that conflicts with the linker:
+
+```sh
+meson setup build -Dnls=false -Dtests=true
+ninja -C build
+```
+
+The `.app` bundle is generated at `build/data/macos/Terminology.app`.
+
+### Installing the .app bundle
+
+Copy the bundle to `/Applications` so that macOS registers the URL scheme
+and the Dock icon:
+
+```sh
+cp -R build/data/macos/Terminology.app /Applications/
+```
+
+After first launch, macOS will recognise the `terminology://` URL scheme
+automatically. You can verify with:
+
+```sh
+open "terminology://"
+```
+
+### Running tests
+
+```sh
+./build/src/bin/tytest all
+```
diff --git a/data/macos/Info.plist.in b/data/macos/Info.plist.in
new file mode 100644
index 00000000..9b1f7087
--- /dev/null
+++ b/data/macos/Info.plist.in
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>terminology</string>
+ <key>CFBundleIconFile</key>
+ <string>@icon_file@</string>
+ <key>CFBundleIdentifier</key>
+ <string>org.enlightenment.terminology</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>Terminology</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>@version@</string>
+ <key>CFBundleVersion</key>
+ <string>@version@</string>
+ <key>NSHighResolutionCapable</key>
+ <true/>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © The Enlightenment Project. Licensed under BSD.</string>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleURLName</key>
+ <string>Terminology URL</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>terminology</string>
+ </array>
+ </dict>
+ </array>
+</dict>
+</plist>
diff --git a/data/macos/gen_icns.sh b/data/macos/gen_icns.sh
new file mode 100755
index 00000000..c75d9323
--- /dev/null
+++ b/data/macos/gen_icns.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+set -e
+
+INPUT="$1"
+OUTPUT="$2"
+
+WORK=$(mktemp -d)
+ICONSET="${WORK}/terminology.iconset"
+mkdir -p "${ICONSET}"
+
+sips -z 16 16 "${INPUT}" --out "${ICONSET}/icon_16x16.png" > /dev/null 2>&1
+sips -z 32 32 "${INPUT}" --out "${ICONSET}/[email protected]" > /dev/null 2>&1
+sips -z 32 32 "${INPUT}" --out "${ICONSET}/icon_32x32.png" > /dev/null 2>&1
+sips -z 64 64 "${INPUT}" --out "${ICONSET}/[email protected]" > /dev/null 2>&1
+sips -z 128 128 "${INPUT}" --out "${ICONSET}/icon_128x128.png" > /dev/null 2>&1
+sips -z 256 256 "${INPUT}" --out "${ICONSET}/[email protected]" > /dev/null 2>&1
+sips -z 256 256 "${INPUT}" --out "${ICONSET}/icon_256x256.png" > /dev/null 2>&1
+sips -z 512 512 "${INPUT}" --out "${ICONSET}/[email protected]" > /dev/null 2>&1
+sips -z 512 512 "${INPUT}" --out "${ICONSET}/icon_512x512.png" > /dev/null 2>&1
+sips -z 1024 1024 "${INPUT}" --out "${ICONSET}/[email protected]" > /dev/null 2>&1
+
+iconutil -c icns -o "${OUTPUT}" "${ICONSET}"
+rm -rf "${WORK}"
diff --git a/data/macos/launcher.c b/data/macos/launcher.c
new file mode 100644
index 00000000..26f7638f
--- /dev/null
+++ b/data/macos/launcher.c
@@ -0,0 +1,17 @@
+#include <stdlib.h>
+#include <unistd.h>
+
+#ifndef TERMINOLOGY_BIN
+# error "TERMINOLOGY_BIN must be defined at compile time"
+#endif
+
+int
+main(int argc, char *argv[])
+{
+ const char *home = getenv("HOME");
+ if (home)
+ chdir(home);
+
+ execv(TERMINOLOGY_BIN, argv);
+ return 1;
+}
diff --git a/data/macos/link_app.sh b/data/macos/link_app.sh
new file mode 100755
index 00000000..d5bc5b12
--- /dev/null
+++ b/data/macos/link_app.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+# Skip when packaging with DESTDIR
+if [ -n "${DESTDIR}" ]; then
+ exit 0
+fi
+
+APP_BUNDLE="${MESON_INSTALL_PREFIX}/Applications/Terminology.app"
+
+# Spotlight does not follow symlinks, so copy the bundle into a
+# directory it indexes.
+if [ -w /Applications ] || [ -w /Applications/Terminology.app ]; then
+ rm -rf /Applications/Terminology.app
+ cp -R "${APP_BUNDLE}" /Applications/Terminology.app
+ echo "Installed /Applications/Terminology.app"
+elif [ -d "${HOME}" ]; then
+ mkdir -p "${HOME}/Applications"
+ rm -rf "${HOME}/Applications/Terminology.app"
+ cp -R "${APP_BUNDLE}" "${HOME}/Applications/Terminology.app"
+ echo "Installed ~/Applications/Terminology.app (use sudo for /Applications)"
+fi
diff --git a/data/macos/meson.build b/data/macos/meson.build
new file mode 100644
index 00000000..9f0f37ef
--- /dev/null
+++ b/data/macos/meson.build
@@ -0,0 +1,51 @@
+app_name = 'Terminology'
+app_bundle = app_name + '.app'
+app_contents = join_paths('Applications', app_bundle, 'Contents')
+
+plist_conf = configuration_data()
+plist_conf.set('version', meson.project_version())
+
+icon_png = files('../icons/terminology.png')
+
+iconutil = find_program('iconutil', required: false)
+sips_prog = find_program('sips', required: false)
+
+if iconutil.found() and sips_prog.found()
+ bash = find_program('bash')
+ plist_conf.set('icon_file', 'terminology')
+ icns = custom_target('terminology_icns',
+ input: icon_png,
+ output: 'terminology.icns',
+ command: [bash, files('gen_icns.sh'), '@INPUT@', '@OUTPUT@'],
+ install: true,
+ install_dir: join_paths(prefix, app_contents, 'Resources')
+ )
+else
+ plist_conf.set('icon_file', 'terminology.png')
+ install_data(icon_png,
+ install_dir: join_paths(app_contents, 'Resources'))
+endif
+
+plist = configure_file(
+ input: 'Info.plist.in',
+ output: 'Info.plist',
+ configuration: plist_conf
+)
+install_data(plist,
+ install_dir: app_contents)
+
+bin_path = join_paths(prefix, get_option('bindir'), 'terminology')
+
+launcher = custom_target('terminology_app_launcher',
+ input: files('launcher.c'),
+ output: 'terminology',
+ command: [
+ cc.cmd_array(), '@INPUT@',
+ '-DTERMINOLOGY_BIN="' + bin_path + '"',
+ '-o', '@OUTPUT@'
+ ],
+ install: true,
+ install_dir: join_paths(prefix, app_contents, 'MacOS')
+)
+
+meson.add_install_script('link_app.sh')
diff --git a/data/meson.build b/data/meson.build
index 62e897c9..4c56ef28 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -5,3 +5,6 @@ subdir('fonts')
subdir('themes')
subdir('backgrounds')
subdir('colorschemes')
+if host_os == 'darwin'
+ subdir('macos')
+endif
diff --git a/meson.build b/meson.build
index 780e94c7..8aac1afe 100644
--- a/meson.build
+++ b/meson.build
@@ -86,6 +86,11 @@ foreach efl_dep: efl_deps
endif
endforeach
+if host_os == 'darwin'
+ ecore_cocoa_dep = dependency('ecore-cocoa', required: true)
+ terminology_dependencies += [ecore_cocoa_dep]
+endif
+
if cc.has_function('mkstemps')
config_data.set('HAVE_MKSTEMPS', 1)
endif
diff --git a/src/bin/main.c b/src/bin/main.c
index 5384e26c..f3f6f510 100644
--- a/src/bin/main.c
+++ b/src/bin/main.c
@@ -20,6 +20,10 @@
#include "gravatar.h"
#include "keyin.h"
+#ifdef __APPLE__
+# include <Ecore_Cocoa.h>
+#endif
+
int terminology_starting_up;
int _log_domain = -1;
Eina_Bool multisense_available = EINA_TRUE;
@@ -185,6 +189,42 @@ main_config_get(void)
return _main_config;
}
+#ifdef __APPLE__
+static Eina_Bool
+_cb_reopen(Eina_Bool has_visible EINA_UNUSED)
+{
+ Win *wn;
+ Term *term;
+
+ wn = win_new(NULL, NULL, NULL, NULL, _main_config,
+ EINA_FALSE, EINA_FALSE, EINA_FALSE, EINA_FALSE, EINA_FALSE);
+ if (!wn)
+ {
+ CRITICAL(_("Could not create window on reopen"));
+ return EINA_FALSE;
+ }
+
+ term = term_new(wn, win_config_get(wn), NULL, EINA_FALSE,
+ NULL, 80, 24, EINA_FALSE, NULL);
+ if (!term)
+ {
+ CRITICAL(_("Could not create terminal on reopen"));
+ win_free(wn);
+ return EINA_FALSE;
+ }
+
+ if (win_term_set(wn, term) < 0)
+ {
+ win_free(wn);
+ return EINA_FALSE;
+ }
+
+ win_sizing_handle(wn);
+ evas_object_show(win_evas_object_get(wn));
+
+ return EINA_TRUE;
+}
+#endif
static void
main_ipc_new(Ipc_Instance *inst)
@@ -624,6 +664,15 @@ _start(Ipc_Instance *instance, Eina_Bool need_scale_wizard)
main_media_update(config);
win_sizing_handle(wn);
win = win_evas_object_get(wn);
+ if (instance->pos)
+ {
+ int screen_w, screen_h;
+
+ elm_win_screen_size_get(win, NULL, NULL, &screen_w, &screen_h);
+ if (instance->x < 0) instance->x = screen_w + instance->x;
+ if (instance->y < 0) instance->y = screen_h + instance->y;
+ evas_object_move(win, instance->x, instance->y);
+ }
evas_object_show(win);
if (instance->startup_split)
{
@@ -657,16 +706,7 @@ _start(Ipc_Instance *instance, Eina_Bool need_scale_wizard)
goto end;
}
}
- }
- if (instance->pos)
- {
- int screen_w, screen_h;
-
- elm_win_screen_size_get(win, NULL, NULL, &screen_w, &screen_h);
- if (instance->x < 0) instance->x = screen_w + instance->x;
- if (instance->y < 0) instance->y = screen_h + instance->y;
- evas_object_move(win, instance->x, instance->y);
- }
+ }
if (instance->nowm)
ecore_evas_focus_set(ecore_evas_ecore_evas_get(
evas_object_evas_get(win)), 1);
@@ -917,6 +957,10 @@ elm_main(int argc, char **argv)
ipc_init();
+#ifdef __APPLE__
+ ecore_cocoa_reopen_cb_set(_cb_reopen);
+#endif
+
instance.config = config_fork(_main_config);
args = ecore_getopt_parse(&options, values, argc, argv);
diff --git a/src/bin/win.c b/src/bin/win.c
index 7ed9f58a..ce2d2779 100644
--- a/src/bin/win.c
+++ b/src/bin/win.c
@@ -1195,6 +1195,135 @@ terms_from_win_object(Evas_Object *win)
return wn->terms;
}
+#ifdef __APPLE__
+/* Find the focused Win, or fall back to list head */
+static Win *
+_macos_focused_win_get(void)
+{
+ Eina_List *l;
+ Win *wn;
+
+ EINA_LIST_FOREACH(wins, l, wn)
+ {
+ if (wn->focused) return wn;
+ }
+ /* No focused window — return list head as last resort */
+ DBG("No focused Win found, falling back to list head");
+ return wins ? (Win *)eina_list_data_get(wins) : NULL;
+}
+
+static void
+_cb_macos_new_window(void *data EINA_UNUSED,
+ Evas_Object *obj EINA_UNUSED,
+ void *event_info EINA_UNUSED)
+{
+ Win *wn;
+ Term *term;
+
+ wn = win_new(NULL, NULL, NULL, NULL, main_config_get(),
+ EINA_FALSE, EINA_FALSE, EINA_FALSE, EINA_FALSE, EINA_FALSE);
+ if (!wn)
+ {
+ ERR("macOS New Window: win_new() failed");
+ return;
+ }
+ /* Initial size hints — win_sizing_handle() below will correct to theme geometry */
+ term = term_new(wn, win_config_get(wn), NULL, EINA_FALSE,
+ NULL, 80, 24, EINA_FALSE, NULL);
+ if (!term)
+ {
+ ERR("macOS New Window: term_new() failed");
+ win_free(wn);
+ return;
+ }
+ if (win_term_set(wn, term) < 0)
+ {
+ ERR("macOS New Window: win_term_set() failed");
+ term_unref(term);
+ win_free(wn);
+ return;
+ }
+ win_sizing_handle(wn);
+ evas_object_show(win_evas_object_get(wn));
+}
+
+static void
+_cb_macos_copy(void *data EINA_UNUSED,
+ Evas_Object *obj EINA_UNUSED,
+ void *event_info EINA_UNUSED)
+{
+ Win *wn;
+ Term *term;
+
+ wn = _macos_focused_win_get();
+ if (!wn) return;
+ if ((!wn->child) || (!wn->child->focused_term_get)) return;
+ term = wn->child->focused_term_get(wn->child);
+ if (!term) return;
+ termio_take_selection(term->termio, ELM_SEL_TYPE_CLIPBOARD);
+}
+
+static void
+_cb_macos_paste(void *data EINA_UNUSED,
+ Evas_Object *obj EINA_UNUSED,
+ void *event_info EINA_UNUSED)
+{
+ Win *wn;
+ Term *term;
+
+ wn = _macos_focused_win_get();
+ if (!wn) return;
+ if ((!wn->child) || (!wn->child->focused_term_get)) return;
+ term = wn->child->focused_term_get(wn->child);
+ if (!term) return;
+ termio_paste_selection(term->termio, ELM_SEL_TYPE_CLIPBOARD);
+}
+
+static void
+_cb_macos_quit(void *data EINA_UNUSED,
+ Evas_Object *obj EINA_UNUSED,
+ void *event_info EINA_UNUSED)
+{
+ elm_exit();
+}
+
+static void
+_win_macos_menu_setup(Evas_Object *win)
+{
+ static Eina_Bool done = EINA_FALSE;
+ Evas_Object *menu;
+ Elm_Object_Item *app_item, *file_item, *edit_item;
+
+ if (done) return;
+
+ menu = elm_win_main_menu_get(win);
+ if (!menu) return;
+ done = EINA_TRUE; /* committed to this menu — prevent duplicate setup */
+
+ /* "Terminology" app menu — macOS convention: first = app name */
+ app_item = elm_menu_item_add(menu, NULL, NULL, "Terminology", NULL, NULL);
+ if (!app_item) return;
+ elm_menu_item_add(menu, app_item, NULL, "Quit Terminology",
+ _cb_macos_quit, NULL);
+
+ /* "File" menu */
+ file_item = elm_menu_item_add(menu, NULL, NULL, "File", NULL, NULL);
+ if (!file_item) return;
+ elm_menu_item_add(menu, file_item, NULL, "New Window",
+ _cb_macos_new_window, NULL);
+
+ /* "Edit" menu */
+ edit_item = elm_menu_item_add(menu, NULL, NULL, "Edit", NULL, NULL);
+ if (!edit_item) return;
+ elm_menu_item_add(menu, edit_item, NULL, "Copy",
+ _cb_macos_copy, NULL);
+ elm_menu_item_add(menu, edit_item, NULL, "Paste",
+ _cb_macos_paste, NULL);
+ /* TODO: investigate setting keyboard equivalents (Cmd+C/V) via Cocoa bridge */
+
+ done = EINA_TRUE;
+}
+#endif /* __APPLE__ */
static Evas_Object *
win_add(const char *name, const char *role,
@@ -1213,6 +1342,10 @@ win_add(const char *name, const char *role,
elm_win_autodel_set(win, EINA_TRUE);
+#ifdef __APPLE__
+ _win_macos_menu_setup(win);
+#endif
+
return win;
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.