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.

Reply via email to