All Projects → fiddlerwoaroof → objc-lisp-bridge

fiddlerwoaroof / objc-lisp-bridge

Licence: other
A portable reader and bridge for interacting with Objective-C and Cocoa

Programming Languages

common lisp
692 projects
Makefile
30231 projects

Labels

Projects that are alternatives of or similar to objc-lisp-bridge

Kstimeline
Simple custom timeline written in swift
Stars: ✭ 157 (+302.56%)
Mutual labels:  cocoa
Dd Plist
A java library providing support for ASCII, XML and binary property lists.
Stars: ✭ 201 (+415.38%)
Mutual labels:  cocoa
Punic
Clean room reimplementation of Carthage tool
Stars: ✭ 236 (+505.13%)
Mutual labels:  cocoa
Mail Notifr
Mail Notifr - Gmail Notifier for macOS
Stars: ✭ 172 (+341.03%)
Mutual labels:  cocoa
Cocoa Rest Client
A free, native Apple macOS app for testing HTTP/REST endpoints
Stars: ✭ 2,257 (+5687.18%)
Mutual labels:  cocoa
Textmate
TextMate is a graphical text editor for macOS 10.12 or later
Stars: ✭ 13,729 (+35102.56%)
Mutual labels:  cocoa
Crosswindow
💻📱 A cross platform system abstraction library written in C++ for managing windows and performing OS tasks.
Stars: ✭ 155 (+297.44%)
Mutual labels:  cocoa
nimble-commander
Nimble Commander - dual-pane file manager for Mac
Stars: ✭ 147 (+276.92%)
Mutual labels:  cocoa
Cocoatextfield
Apple TextField created according to the Material.IO guidelines of 2019. Featured at Medium.
Stars: ✭ 195 (+400%)
Mutual labels:  cocoa
Criollo
A powerful Cocoa web framework and HTTP server for macOS, iOS and tvOS.
Stars: ✭ 229 (+487.18%)
Mutual labels:  cocoa
Wwdc Notes
WWDCNotes.com content ✨
Stars: ✭ 183 (+369.23%)
Mutual labels:  cocoa
Widgetkit
Compose native apps without a code using JSON and load them as NSBundle into another app dynamicly from local or remote locations.
Stars: ✭ 191 (+389.74%)
Mutual labels:  cocoa
Cacao
Rust bindings for AppKit (macOS) and UIKit (iOS/tvOS). Experimental, but working!
Stars: ✭ 205 (+425.64%)
Mutual labels:  cocoa
Qlmoviepreview
QuickLook plugin to display movie thumbnail and video informations. Also create nice Finder icons from the thumbnail.
Stars: ✭ 162 (+315.38%)
Mutual labels:  cocoa
react-native-macos
Fork of https://github.com/ptmt/react-native-macos with more features
Stars: ✭ 22 (-43.59%)
Mutual labels:  cocoa
Usbdeviceswift
wrapper for IOKit.usb and IOKit.hid written on pure Swift that allows you convenient work with USB devices
Stars: ✭ 156 (+300%)
Mutual labels:  cocoa
Pywebview
Build GUI for your Python program with JavaScript, HTML, and CSS
Stars: ✭ 2,649 (+6692.31%)
Mutual labels:  cocoa
OutlineViewDiffableDataSource
Stop looking for NSOutlineViewDiffableDataSource, it’s here 👌
Stars: ✭ 96 (+146.15%)
Mutual labels:  cocoa
panthro
An implementation of XPath 3.0 in Objective-C/Cocoa
Stars: ✭ 45 (+15.38%)
Mutual labels:  cocoa
Carthage
A simple, decentralized dependency manager for Cocoa
Stars: ✭ 14,487 (+37046.15%)
Mutual labels:  cocoa

Intro

CCL and LispWorks and other implementations have their own bridges to the objective-c runtime. This project is an attempt to create a bridge that only uses CFFI so that arbitrary lisp implementations can produce native mac GUIs. In the long run, I hope to use this as the basis for a new mac-native backend for McClim: but we’ll see if that ever happens.

For the time being, though, this only works on CCL and (sort-of) on LispWorks: it works like 95% on SBCL, but there’s some weird issue that’s preventing the window from showing. I hae not tested the code on any other implementations, but doing so will require changing a couple places in objc-runtime.lisp to inform the code about the new lisp’s ffi types.

Installing

  1. clone fwoar.lisputils from https://github.com/fiddlerwoaroof/fwoar.lisputils and put it somewhere quicklisp can find it (e.g. ~/quicklisp/local-projects)
  2. clone cffi from https://github.com/cffi/cffi and put it in the same place (on Big Sur, at least, I need changes that haven’t made it to Quicklisp)
  3. Install rsvg-convert:
    brew install librsvg
        
  4. build + run the demo:
    make mkapp CL=/path/to/cl
    open demo.app
        

Show me the code!

From demo-app.lisp:

(defun main ()
  (trivial-main-thread:with-body-in-main-thread (:blocking t)
    [#@NSAutoReleasePool @(new)]
    [#@NSApplication @(sharedApplication)]
    [objc-runtime::ns-app @(setActivationPolicy:) :int 0]

    (objc-runtime::objc-register-class-pair
     (demo-app::make-app-delegate-class '("actionButton"
                                "alertButton"
                                "profitButton")))

    (demo-app::load-nib "MainMenu")

    (let ((app-delegate [objc-runtime::ns-app @(delegate)]))
      (demo-app::make-button-delegate (value-for-key app-delegate "actionButton")
                            (cffi:callback do-things-action))
      (demo-app::make-button-delegate (value-for-key app-delegate "alertButton")
                            (cffi:callback alert-action))
      (demo-app::make-button-delegate (value-for-key app-delegate "profitButton")
                            (cffi:callback profit-action)))

    [objc-runtime::ns-app @(activateIgnoringOtherApps:) :boolean t]
    [objc-runtime::ns-app @(run)]))

In-depth example

Type-directed Objective-C extractors

(defvar *objc-extractors* (list)
  "Functions called to extract specific data types")

(defun extract-from-objc (obj)
  (objc-typecase obj
    (#@NSDate [[[[#@NSISO8601DateFormatter @(alloc)]
                 @(init)]
                @(stringFromDate:) :pointer obj]
               @(UTF8String)]s)
    (#@NSString [obj @(UTF8String)]s)
    (#@NSNumber (parse-number:parse-number
                 (objc-runtime::extract-nsstring
                  [obj @(stringValue)])))
    (#@NSArray (map-nsarray #'extract-from-objc obj))
    (#@NSDictionary (fw.lu:alist-string-hash-table
                     (pairlis (map-nsarray #'extract-from-objc [obj @(allKeys)])
                              (map-nsarray #'extract-from-objc [obj @(allValues)]))))
    (t (or (funcall-some (cdr (objc-pick-by-type obj *objc-extractors*))
                         obj)
           obj))))

(defmacro define-extractor (class (o) &body body)
  `(serapeum:eval-always
     (add-extractor ,class
                    (lambda (,o)
                      ,@body))
     *objc-extractors*))

(defun clear-extractors ()
  (setf *objc-extractors* ()))

(serapeum:eval-always
  (defun add-extractor (class cb)
    (unless (member class *objc-extractors* :test 'cffi:pointer-eq :key #'car)
      (setf *objc-extractors*
            (merge 'list *objc-extractors* (list (cons class cb))
                   'objc-subclass-p
                   :key 'car)))
    *objc-extractors*))

Reading List to Org-file converter

The entry-point is fairly unremarkable: it delegates most of the work to other functions and disables the debugger so that this doesn’t blow up when an error occurs in non-interactive mode.

(defun main ()
  <<disable-sbcl-debugger>>
  (make-org-file *standard-output*
                 (get-readinglist-info
                  (translate-plist
                   (get-bookmark-filename)))))

This pair of functions builds an org file from data extracted from the Safari bookmark file.

(defun make-org-file (s reading-list-info)
  (format s "~&* Safari Reading List~%")
  (serapeum:mapply (serapeum:partial 'make-org-entry s)
                   reading-list-info))

(defun make-org-entry (s date title url preview tag)
  (format s "~&** ~a (~a) :~{~a:~}~% ~a~2% ~{~<~% ~1,80:;~a~> ~}~2%"
          title
          (local-time:format-timestring nil date
                                        :format local-time:+rfc3339-format/date-only+)
          (alexandria:ensure-list tag)
          url
          (serapeum:tokens preview)))

Here we extract the data from Bookmarks.plist using our polymorphic objc data extractor framework

(defparameter *reading-list-location* "Library/Safari/Bookmarks.plist")
(defun get-bookmark-filename ()
  (uiop:native-namestring
   (merge-pathnames *reading-list-location*
                    (truename "~/"))))

(defun translate-plist (fn)
  (objc-runtime.data-extractors:extract-from-objc
   (objc-runtime.data-extractors:get-plist fn)))
(defun get-readinglist-info (bookmarks)
  (sort (mapcar 'extract-link-info
                (gethash "Children"
                         (car
                          (select-child bookmarks
                                        "com.apple.ReadingList"))))
        'local-time:timestamp>
        :key 'car))

(defun extract-link-info (link)
  (list (local-time:parse-rfc3339-timestring (or (fw.lu:pick '("ReadingList" "DateAdded") link)
                                                 (fw.lu:pick '("ReadingList" "DateLastViewed") link)
                                                 (fw.lu:pick '("ReadingListNonSync" "DateLastFetched") link)
                                                 (local-time:now)))
        (fw.lu:pick '("URIDictionary" "title") link)
        (fw.lu:pick '("URLString") link)
        (plump:decode-entities (coerce (fw.lu:pick '("ReadingList" "PreviewText") link) 'simple-string) t)
        (fw.lu:may (slugify (fw.lu:pick '("ReadingListNonSync" "siteName") link)))))

Appendices

objc-data-extractor.lisp

(defpackage :objc-runtime.data-extractors
  (:use :cl )
  (:export
   #:extract-from-objc
   #:define-extractor
   #:clear-extractors
   #:add-extractor
   #:get-plist))

(in-package :objc-runtime.data-extractors)
(named-readtables:in-readtable :objc-readtable)

(defun get-plist (file)
  [#@NSDictionary @(dictionaryWithContentsOfFile:)
                  :pointer (objc-runtime::make-nsstring file)])

(defun objc-subclass-p (sub super)
  (unless (or (cffi:null-pointer-p sub)
              (cffi:null-pointer-p super))
    (or (eql sub super)
        (= [sub @(isSubclassOfClass:) :pointer [super @(class)]]#
           1))))

(defun order-objc-classes (classes &rest r &key key)
  (declare (ignore key))
  (apply 'stable-sort
         (copy-seq classes)
         'objc-subclass-p
         r))

(defun objc-isa (obj class)
  (unless (or (cffi:null-pointer-p obj)
              (cffi:null-pointer-p class))
    (= [obj @(isKindOfClass:) :pointer class]#
       1)))

(defun objc-pick-by-type (obj pairs)
  (assoc obj
         (order-objc-classes pairs :key 'car)
         :test 'objc-isa))

(serapeum:eval-always
  (defun make-cases (cases obj)
    (mapcar (serapeum:op
              `(if (objc-isa ,obj ,(car _1))
                   (progn ,@(cdr _1))))
                   cases)))

(defmacro objc-typecase (form &body ((case-type &body case-handler) &rest cases))
  (alexandria:once-only (form)
    (let* ((initial-cases `((,case-type ,@case-handler) ,@(butlast cases)))
           (cases (fw.lu:rollup-list (make-cases initial-cases form)
                                     (if (eql t (caar (last cases)))
                                         `((progn ,@(cdar (last cases))))
                                         (make-cases (last cases) form)))))
      cases)))

(defun map-nsarray (fn arr)
  (unless (and (cffi:pointerp arr)
               (objc-isa arr #@NSArray))
    (error "must provide a NSArray pointer"))
  (loop for x below [arr @(count)]#
     collect (funcall fn [arr @(objectAtIndex:) :int x])))

(defun nsarray-contents (arr)
  (unless (and (cffi:pointerp arr)
               (objc-isa arr #@NSArray))
    (error "must provide a NSArray pointer"))
  (dotimes (n [arr @(count)]#)
    (let ((obj [arr @(objectAtIndex:) :int n ]))
      (objc-typecase obj
        (#@NSString (format t "~&string~%"))
        (#@NSArray (format t "~&array~%"))
        (#@NSDictionary (format t "~&dictionary~%"))
        (t (format t "~&other... ~s~%" (objc-runtime::objc-class-get-name
                                        (objc-runtime::object-get-class obj))))))))

(defmacro funcall-some (fun &rest args)
  (alexandria:once-only (fun)
    `(if ,fun
         (funcall ,fun ,@args))))

<<extractor-framework>>

build-reading-list-reader.sh

#!/usr/bin/env bash
set -eu -x -o pipefail

cd "$(dirname $0)"
mkdir -p dist

pushd dist
rm -rf fwoar.lisputils
git clone https://github.com/fiddlerwoaroof/fwoar.lisputils.git
popd

export CL_SOURCE_REGISTRY="$PWD/dist//"
sbcl --no-userinit \
     --load ~/quicklisp/setup.lisp \
     --load build.lisp

build.lisp

(eval-when (:compile-toplevel :load-toplevel :execute)
  (setf *default-pathname-defaults* (truename "~/git_repos/objc-lisp-bridge/"))
  (load (compile-file "objc-runtime.asd")))

(eval-when (:compile-toplevel :load-toplevel :execute)
  (ql:quickload '(:objc-runtime :yason :plump :cl-ppcre)))

(load "reading-list-reader.lisp")

(eval-when (:compile-toplevel :load-toplevel :execute)
  (sb-ext:save-lisp-and-die "reading-list2org"
                            :toplevel (intern "MAIN"
                                              "READING-LIST-READER")
                            :executable t))

reading-list-reader.lisp

(defpackage :reading-list-reader
  (:use :cl )
  (:export ))
(in-package :reading-list-reader)

(serapeum:eval-always
  (named-readtables:in-readtable :objc-readtable))

(defun slugify (s)
  (cl-ppcre:regex-replace-all "\\s+"
                              (string-downcase s)
                              "_"))

(defun select-child (d title)
  (flet ((get-title (h)
           (equal (gethash "Title" h)
                  title)))
    (fw.lu:let-each (:be *)
      (gethash "Children" d)
      (remove-if-not #'get-title *))))

<<translate-plist>>

<<make-org-file>>

<<translate-data>>

<<r-l-r-main>>
#+(and build sbcl)
(progn (sb-ext:disable-debugger)
       (sb-alien:alien-funcall
        (sb-alien:extern-alien "disable_lossage_handler"
                               (function sb-alien:void))))
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].