Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New notifer for Linux which use libnotify and PHP-FFI #100

Merged
merged 4 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
pyrech marked this conversation as resolved.
Show resolved Hide resolved
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
Loading