Skip to content

Commit

Permalink
Merge pull request #100 from morawskim/libnotify
Browse files Browse the repository at this point in the history
New notifer for Linux which use libnotify and PHP-FFI
  • Loading branch information
pyrech authored Apr 29, 2024
2 parents 3d3aec7 + c9330a7 commit 3647c3c
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 6 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, xml
extensions: mbstring, xml, ffi
ini-values: phar.readonly="Off"

- name: Get composer cache directory
Expand All @@ -71,6 +71,10 @@ jobs:
run: |
composer update --prefer-dist --no-interaction ${{ matrix.composer-flags }}
- name: Install libnotify4 for LibNotifyNotifier
run: |
sudo apt-get install -y --no-install-recommends --no-install-suggests libnotify4
- name: Run Tests
run: php vendor/bin/simple-phpunit

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Not released yet

* Added wsl-notify-send notifier for Windows Subsystem for Linux
* Added libnotify based notifier for Linux through FFI
* Changed TerminalNotifier to use contentImage option for icon instead of appIcon
* Fixed phar missing some dependencies

Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"symfony/finder": "^5.4 || ^6.0 || ^7.0",
"symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0"
},
"suggest": {
"ext-ffi": "Needed to send notifications via libnotify on Linux"
},
"bin": [
"jolinotif"
],
Expand Down
7 changes: 7 additions & 0 deletions doc/03-notifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ distributions.

notify-send can display notification with a body, a title and an icon.

##### LibNotifyNotifier

This notifier use the FFI PHP extension.
The C library `libnotify` should be installed by default on most Linux distributions wih graphical interface.

LibNotifyNotifier can display notification with a body, a title and an icon.

### Mac OS

#### GrowlNotifyNotifier
Expand Down
2 changes: 2 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ parameters:
checkGenericClassInNonGenericObjectType: false
excludePaths:
analyse: []
ignoreErrors:
- '#Call to an undefined method FFI::.+#'
16 changes: 16 additions & 0 deletions src/Exception/FFIRuntimeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/*
* This file is part of the JoliNotif project.
*
* (c) Loïck Piera <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Joli\JoliNotif\Exception;

class FFIRuntimeException extends \RuntimeException implements Exception
{
}
13 changes: 13 additions & 0 deletions src/Notifier/FFI/ffi-libnotify.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#define FFI_LIB "libnotify.so.4"

typedef bool gboolean;
typedef void* gpointer;
typedef struct _NotifyNotification NotifyNotification;
typedef struct _GTypeInstanceError GError;

gboolean notify_init(const char *app_name);
gboolean notify_is_initted (void);
void notify_uninit (void);
NotifyNotification *notify_notification_new(const char *summary, const char *body, const char *icon);
gboolean notify_notification_show (NotifyNotification *notification, GError **error);
void g_object_unref (gpointer object);
92 changes: 92 additions & 0 deletions src/Notifier/LibNotifyNotifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

/*
* This file is part of the JoliNotif project.
*
* (c) Loïck Piera <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Joli\JoliNotif\Notifier;

use Joli\JoliNotif\Exception\FFIRuntimeException;
use Joli\JoliNotif\Exception\InvalidNotificationException;
use Joli\JoliNotif\Notification;
use Joli\JoliNotif\Notifier;
use JoliCode\PhpOsHelper\OsHelper;

class LibNotifyNotifier implements Notifier
{
private static string $APP_NAME = 'jolinotif';

private \FFI $ffi;

public function __destruct()
{
if (isset($this->ffi)) {
$this->ffi->notify_uninit();
}
}

public static function isLibraryExists(): bool
{
return file_exists('/lib64/libnotify.so.4')
|| file_exists('/lib/x86_64-linux-gnu/libnotify.so.4');
}

public function isSupported(): bool
{
return OsHelper::isUnix()
&& !OsHelper::isMacOS()
&& class_exists(\FFI::class)
&& self::isLibraryExists();
}

public function getPriority(): int
{
return static::PRIORITY_HIGH;
}

public function send(Notification $notification): bool
{
if (!$notification->getBody()) {
throw new InvalidNotificationException($notification, 'Notification body can not be empty');
}

$this->initialize();
$notification = $this->ffi->notify_notification_new(
$notification->getTitle() ?? '',
$notification->getBody(),
$notification->getIcon()
);
$value = $this->ffi->notify_notification_show($notification, null);
$this->ffi->g_object_unref($notification);

return $value;
}

private function initialize(): void
{
if (isset($this->ffi)) {
return;
}

$ffi = \FFI::load(__DIR__ . '/FFI/ffi-libnotify.h');

if (!$ffi) {
throw new FFIRuntimeException('Unable to load libnotify');
}

$this->ffi = $ffi;

if (!$this->ffi->notify_init(self::$APP_NAME)) {
throw new FFIRuntimeException('Unable to initialize libnotify');
}

if (!$this->ffi->notify_is_initted()) {
throw new FFIRuntimeException('Libnotify has not been initialized');
}
}
}
2 changes: 2 additions & 0 deletions src/NotifierFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Joli\JoliNotif\Notifier\AppleScriptNotifier;
use Joli\JoliNotif\Notifier\GrowlNotifyNotifier;
use Joli\JoliNotif\Notifier\KDialogNotifier;
use Joli\JoliNotif\Notifier\LibNotifyNotifier;
use Joli\JoliNotif\Notifier\NotifuNotifier;
use Joli\JoliNotif\Notifier\NotifySendNotifier;
use Joli\JoliNotif\Notifier\NullNotifier;
Expand Down Expand Up @@ -75,6 +76,7 @@ public static function getDefaultNotifiers(): array
private static function getUnixNotifiers(): array
{
return [
new LibNotifyNotifier(),
new GrowlNotifyNotifier(),
new TerminalNotifierNotifier(),
new AppleScriptNotifier(),
Expand Down
5 changes: 0 additions & 5 deletions tests/Notifier/CliBasedNotifierTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,6 @@ public function testSendThrowsExceptionWhenNotificationHasAnEmptyBody()
}
}

public function getIconDir(): string
{
return realpath(\dirname(__DIR__) . '/fixtures');
}

abstract protected function getExpectedCommandLineForNotification(): string;

abstract protected function getExpectedCommandLineForNotificationWithATitle(): string;
Expand Down
109 changes: 109 additions & 0 deletions tests/Notifier/LibNotifyNotifierTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

/*
* This file is part of the JoliNotif project.
*
* (c) Loïck Piera <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Joli\JoliNotif\tests\Notifier;

use Joli\JoliNotif\Exception\InvalidNotificationException;
use Joli\JoliNotif\Notification;
use Joli\JoliNotif\Notifier;
use Joli\JoliNotif\Notifier\LibNotifyNotifier;

class LibNotifyNotifierTest extends NotifierTestCase
{
public function testGetPriority()
{
$notifier = $this->getNotifier();

$this->assertSame(Notifier::PRIORITY_HIGH, $notifier->getPriority());
}

public function testSendWithEmptyBody()
{
$notifier = $this->getNotifier();

$this->expectException(InvalidNotificationException::class);
$this->expectExceptionMessage('Notification body can not be empty');
$notifier->send(new Notification());
}

/**
* @requires extension ffi
*/
public function testInitialize()
{
$notifier = $this->getNotifier();

if (!$notifier::isLibraryExists()) {
$this->markTestSkipped('Looks like libnotify is not installed');
}

$this->assertTrue($notifier->isSupported());
}

public function testSendThrowsExceptionWhenNotificationDoesntHaveBody()
{
$notifier = $this->getNotifier();

$notification = new Notification();

try {
$notifier->send($notification);
$this->fail('Expected a InvalidNotificationException');
} catch (\Exception $e) {
$this->assertInstanceOf('Joli\JoliNotif\Exception\InvalidNotificationException', $e);
}
}

public function testSendThrowsExceptionWhenNotificationHasAnEmptyBody()
{
$notifier = $this->getNotifier();

$notification = new Notification();
$notification->setBody('');

try {
$notifier->send($notification);
$this->fail('Expected a InvalidNotificationException');
} catch (\Exception $e) {
$this->assertInstanceOf('Joli\JoliNotif\Exception\InvalidNotificationException', $e);
}
}

/**
* @requires extension ffi
*/
public function testSendNotificationWithAllOptions()
{
$notifier = $this->getNotifier();

$notification = (new Notification())
->setBody('I\'m the notification body')
->setTitle('I\'m the notification title')
->addOption('subtitle', 'I\'m the notification subtitle')
->addOption('sound', 'Frog')
->addOption('url', 'https://google.com')
->setIcon($this->getIconDir() . '/image.gif')
;

$result = $notifier->send($notification);

if (!$result) {
$this->markTestSkipped('Notification was not sent');
}

$this->assertTrue($notifier->send($notification));
}

protected function getNotifier(): LibNotifyNotifier
{
return new LibNotifyNotifier();
}
}
5 changes: 5 additions & 0 deletions tests/Notifier/NotifierTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ abstract class NotifierTestCase extends TestCase
{
abstract protected function getNotifier(): Notifier;

protected function getIconDir(): string
{
return realpath(\dirname(__DIR__) . '/fixtures');
}

/**
* Call protected/private method of a class.
*
Expand Down
3 changes: 3 additions & 0 deletions tests/NotifierFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Joli\JoliNotif\Notifier\AppleScriptNotifier;
use Joli\JoliNotif\Notifier\GrowlNotifyNotifier;
use Joli\JoliNotif\Notifier\KDialogNotifier;
use Joli\JoliNotif\Notifier\LibNotifyNotifier;
use Joli\JoliNotif\Notifier\NotifuNotifier;
use Joli\JoliNotif\Notifier\NotifySendNotifier;
use Joli\JoliNotif\Notifier\NullNotifier;
Expand All @@ -34,6 +35,7 @@ public function testGetDefaultNotifiers()

if (OsHelper::isUnix()) {
$expectedNotifierClasses = [
LibNotifyNotifier::class,
GrowlNotifyNotifier::class,
TerminalNotifierNotifier::class,
AppleScriptNotifier::class,
Expand Down Expand Up @@ -63,6 +65,7 @@ public function testCreateUsesDefaultNotifiers()

if (OsHelper::isUnix()) {
$expectedNotifierClasses = [
LibNotifyNotifier::class,
GrowlNotifyNotifier::class,
TerminalNotifierNotifier::class,
AppleScriptNotifier::class,
Expand Down

0 comments on commit 3647c3c

Please sign in to comment.