 | 级别: 初级 Mikael Hallendal (micke@imendio.com), 软件工程师, Imendio Richard Hult (richard@imendio.com), 软件工程师, Imendio
2003 年 12 月 01 日 本文描述了如何使用 GnomeVFS(一个用于访问不同文件系统的 C 库)扩展 GNOME 以及开发自己的虚拟文件系统扩展。本文围绕一个假想的、可以用来访问内存中目录树的示例文件系统展开。
本文解释了如何通过编写自己的 GnomeVFS 模块扩展 GNOME。在
本文作者以前一篇文章
中已经对 GnomeVFS 进行了描述,所以如果需要补充有关 GnomeVFS 的知识,请务必阅读那篇文章。
本文中我们将使用一个带有文本文件的硬编码目录树的例子。您将能够在 Nautilus 中浏览树,并可以将文本文件从位置“tutorial:///”拖入或者拖出。
什么是 GnomeVFS 模块?
GnomeVFS 模块是一个插件程序,它支持对不同服务的访问,就像这些服务是文件系统的一部分一样。例如,HTTP 模块让您可以通过“http:”URI访问
Web 页面。GnomeVFS 包括若干个模块,如 http、ftp和tar。
一些 GnomeVFS 模块如 fontilus 还可以完成一些不那么显眼的工作。Fontilus 让您可以用 Nautilus 在位置“fonts:///”浏览系统中可用的字体,并且可以通过拖放添加和删除字体。图1显示了使用中的
fontilus。GnomeVFS 模块的另一个例子是“applications:///”的处理程序 ,它让您可以将应用程序菜单当成文件系统进行浏览。
图1. Nautilus用字体方法显示字体
您可能会问,将一组可用字体作为一个文件系统展示出来是否有用。一方面,假如您想要访问字体就像访问真实文件系统中的物理文件那样,那可能会出现混乱。另一方面,通过已经熟悉的文件管理器窗口用户界面访问所有字体是非常方便的。
在考虑将某种功能实现为 GnomeVFS 模块时,应该先问问自己这是不是解决手头问题的最佳方式。一个可移植的 MP3 播放器接口会是一个使用VFS模块的好例子,这里模块将底层硬件表现为音乐文件的常规目录结构。在您的数据与文件系统之间是否有自然的联系?通过文件系统访问数据的能力是否真的有用?如果答案是否定的,那么您应该考虑转而编写专门的应用程序来处理数据。
编写模块
GnomeVFS 模块是在运行时装载的共享对象文件(.so 库)。我们现在介绍实现一个简单模块必需的步骤,您可以将它作为编写真实模块的起点。
开始
第一次装载时,调用
vfs_module_init 函数。
清单 1. vfs_module_init()
GnomeVFSMethod *
vfs_module_init (const char *method_name, const char *args)
{
if (strcmp (method_name, "tutorial") == 0) {
init_fake_tree ();
return &method;
}
return NULL;
}
|
这是一个设置模块及初始化模块中需要的变量或者数据的好地方。我们通过调用
init_fake_tree 完成这项工作。这个函数还应该返回一个指向
GnomeVFSMethod 结构的指针,指定用哪个函数打开或者关闭文件、列出目录等。这样的函数表常称为“vtable”或者虚拟表。
vtable 包含不少函数,但是根据模块的复杂程度,只需要实现其中一部分。因为我们的示例模块很简单,所以我们不用实现那么多函数。
清单2. 设置 GnomeVFSMethod 结构的一个例子
static GnomeVFSMethod method = {
sizeof (GnomeVFSMethod),
do_open, /* open */
do_create, /* create */
do_close, /* close */
do_read, /* read */
do_write, /* write */
NULL, /* seek */
NULL, /* tell */
NULL, /* truncate_handle */
do_open_directory, /* open_directory */
do_close_directory, /* close_directory */
do_read_directory, /* read_directory */
do_get_file_info, /* get_file_info */
NULL, /* get_file_info_from_handle */
do_is_local, /* is_local */
do_make_directory, /* make_directory */
do_remove_directory, /* remove_directory */
NULL, /* move */
do_unlink, /* unlink */
NULL, /* check_same_fs */
NULL, /* set_file_info */
NULL, /* truncate */
NULL, /* find_directory */
NULL, /* create_symbolic_link */
NULL, /* monitor_add */
NULL, /* monitor_cancel */
NULL /* file_control */
};
|
正如您看到的,许多函数设置为
NULL ,表明它们未被实现。
我们实现的这些函数应该没有什么可解释的。GnomeVFS 是按照标准 POSIX API 构建的,所以对于已经熟悉在类似 UNIX 的系统中进行常规
C 编程的人来说,应该没有太多的意外之处。
卸载模块时,调用函数
vfs_module_shutdown 以清除模块使用的所有内存。
清单3. vfs_module_shutdown()
void
vfs_module_shutdown (GnomeVFSMethod* method)
{
free_fake_tree ();
}
|
关闭函数释放在初始化函数中建立的伪目录树。
读取目录
为了使这个文件系统起作用,必须能够枚举文件和目录,并提取它们的文件名及其他信息。在我们的 VFS 模块中,首先实现目录处理,再用 Nautilus
浏览我们的文件系统以验证其工作正常。
对目录内容的读取分为三部分:
do_open_directory、
do_read_directory 和
close_directory 。这些都是很直观的函数,所以让我们继续往下实现它们!
首先,您会注意到这三个函数有同样的返回类型
GnomeVFSResult 。所有 VFS 函数都是如此,并且如果成功操作的话,返回值应该是
GNOME_VFS_OK 。对于失败则有许多不同的错误代码,可以查看 API 参考文档 (见
参考资料中的链接)中的完整列表。
在
do_open_directory 中,我们设置所有在读取过程中需要的数据结构。
清单4. do_open_directory()
static GnomeVFSResult
do_open_directory (GnomeVFSMethod *method,
GnomeVFSMethodHandle **method_handle,
GnomeVFSURI *uri,
GnomeVFSFileInfoOptions options,
GnomeVFSContext *context)
{
DirHandle *handle;
FakeNode *file;
handle = g_new0 (DirHandle, 1);
handle->options = options;
file = get_fake_node_from_uri (uri);
if (file) {
handle->gnode = file->gnode;
handle->current_child = handle->gnode->children;
} else {
return GNOME_VFS_ERROR_NOT_FOUND;
}
*method_handle = (GnomeVFSMethodHandle *) handle;
return GNOME_VFS_OK;
}
|
GnomeVFSMethodHandle 参数就是一个有效的指针,可以用它传递您认为合适的任何指针。在我们的例子中,我们将定义一个小的struct,它在遍历目录内容时记录状态。
函数
do_read_directory 将被反复调用,直到返回
GNOME_VFS_ERROR_EOF
,则 表明不再有其他项。
清单5. do_read_directory()
static GnomeVFSResult
do_read_directory (GnomeVFSMethod *method,
GnomeVFSMethodHandle *method_handle,
GnomeVFSFileInfo *file_info,
GnomeVFSContext *context)
{
DirHandle *handle = (DirHandle *) method_handle;
FakeNode *file;
if (!handle->current_child) {
return GNOME_VFS_ERROR_EOF;
}
file = handle->current_child->data;
if (file->directory) {
file_info->type = GNOME_VFS_FILE_TYPE_DIRECTORY;
file_info->valid_fields |= GNOME_VFS_FILE_INFO_FIELDS_TYPE;
file_info->mime_type = g_strdup ("x-directory/normal");
file_info->valid_fields |= GNOME_VFS_FILE_INFO_FIELDS_MIME_TYPE;
} else {
file_info->type = GNOME_VFS_FILE_TYPE_REGULAR;
file_info->valid_fields |= GNOME_VFS_FILE_INFO_FIELDS_TYPE;
file_info->mime_type = g_strdup ("text/plain");
file_info->valid_fields |= GNOME_VFS_FILE_INFO_FIELDS_MIME_TYPE;
file_info->size = file->size;
file_info->valid_fields |= GNOME_VFS_FILE_INFO_FIELDS_SIZE;
}
file_info->name = g_strdup (file->name);
handle->current_child = handle->current_child->next;
return GNOME_VFS_OK;
}
|
具有上面提到的状态的结构,将被传递给这个函数,这与
do_open_directory 类似。
对于在目录中找到的每一项,我们在
GnomeVFSFileInfo 结构中填入收到的有关它的信息。可以设置大量的信息,但是幸运的是,可以只设置最重要的内容,如文件名、文件类型、MIME类型以及文件大小,而不用理会其他的信息。如果是真正的
VFS 模块,可能还需要设置像修改时间、权限等内容。
最后,我们必须关闭目录。
清单6. do_close_directory()
static GnomeVFSResult
do_close_directory (GnomeVFSMethod *method,
GnomeVFSMethodHandle *method_handle,
GnomeVFSContext *context)
{
DirHandle *handle = (DirHandle *) method_handle;
g_free (handle);
return GNOME_VFS_OK;
}
|
do_close_directory 函数很简单,它只不过让我们有机会释放在打开和读取时所占用的任何资源。
好了,到目前为止,一切都还好。现在我们需要实现文件打开和读取,在完成后我们将可以对这个模块进行一些测试。
读取文件
就像目录处理一样,我们需要对语言件进行打开、读取和关闭。
首先需要打开文件。这通过
do_open 函数来完成:
清单7. do_open()
static GnomeVFSResult
do_open (GnomeVFSMethod *method,
GnomeVFSMethodHandle **method_handle,
GnomeVFSURI *uri,
GnomeVFSOpenMode mode,
GnomeVFSContext *context)
{
FakeNode *file;
FileHandle *handle;
file = get_fake_node_from_uri (uri);
if (file && file->directory) {
return GNOME_VFS_ERROR_IS_DIRECTORY;
}
/* We don't support random mode. */
if (mode & GNOME_VFS_OPEN_RANDOM) {
return GNOME_VFS_ERROR_INVALID_OPEN_MODE;
}
if (mode & GNOME_VFS_OPEN_WRITE) {
/* Only handle reading so far */
/* Add the code to write here later in the tutorial */
return GNOME_VFS_ERROR_READ_ONLY;
} else if (mode & GNOME_VFS_OPEN_READ) {
file = get_fake_node_from_uri (uri);
if (!file) {
return GNOME_VFS_ERROR_NOT_FOUND;
}
} else {
return GNOME_VFS_ERROR_INVALID_OPEN_MODE;
}
handle = g_new0 (FileHandle, 1);
handle->fnode = file;
*method_handle = (GnomeVFSMethodHandle *) handle;
return GNOME_VFS_OK;
}
|
do_open 函数工作方式与目录处理非常相似,但是正如您所看到的,它还有一个
GnomeVFSOpenMode
标志,该标志用于指定模式。它应该是
GNOME_VFS_OPEN_READ 或者
GNOME_VFS_OPEN_WRITE
或者同时是这两者。还有一个
GNOME_VFS_OPEN_RANDOM 的值,不过它超出了本文讨论的范围。
我们继续创建储存读取操作状态的句柄,这类似于上面描述的目录处理。
如果 URI 指向一个目录,那么类型标志就设置为
GNOME_VFS_FILE_TYPE_DIRECTORY, 以向调用者表明它们要读取的是一个目录。
打开文件后,
do_read 函数将负责读取文件中的实际数据。
清单8. do_read()
static GnomeVFSResult
do_read (GnomeVFSMethod *method,
GnomeVFSMethodHandle *method_handle,
gpointer buffer,
GnomeVFSFileSize bytes,
GnomeVFSFileSize *bytes_read,
GnomeVFSContext *context)
{
FileHandle *handle = (FileHandle *) method_handle;
if (!handle->str) {
/* This is the first pass, get the content string. */
handle->str = g_strdup (handle->fnode->content);
handle->size = handle->fnode->size;
handle->bytes_written = 0;
}
if (handle->bytes_written >= handle->len) {
/* The whole file is read, return EOF. */
*bytes_read = 0;
return GNOME_VFS_ERROR_EOF;
}
*bytes_read = MIN (bytes, handle->size - handle->bytes_written);
memcpy (buffer, handle->str + handle->bytes_written, *bytes_read);
handle->bytes_written += *bytes_read;
return GNOME_VFS_OK;
}
|
在
do_read 函数中,我们需要处理读取的内容大于缓存容量的情况。在这种情况下,将再次调用这个函数,不断重复,直到读完整个文件。当操作完成后,返回
GNOME_VFS_ERROR_EOF, 表明到达了文件结尾。
最后关闭文件。
清单9. do_close()
static GnomeVFSResult
do_close (GnomeVFSMethod *method,
GnomeVFSMethodHandle *method_handle,
GnomeVFSContext *context)
{
FileHandle *handle = (FileHandle *) method_handle;
g_free (handle->str);
g_free (handle);
return GNOME_VFS_OK;
}
|
do_close 函数只用于进行清理工作,所以在这里我们应该释放在
do_open 中创建的句柄。
在开始试验这个模块之前,我们还必须编写两个函数:
do_is_local 和
do_get_file_info 。
清单10. do_is_local()
static gboolean
do_is_local (GnomeVFSMethod *method,
const GnomeVFSURI *uri)
{
return TRUE;
}
|
do_is_local 用于检查一个 URI 是否是本地文件系统。它的作用是确定是否要在远程文件系统上完成有可能是很慢的工作,比如生成图像的微缩图。如果判断文件是本地的,则应该返回
TRUE ,否则返回
FALSE 。
由于我们现在有一个内存中的伪文件系统,所以我们将它宣告为本地系统。
清单11. do_get_file_info()
static GnomeVFSResult
do_get_file_info (GnomeVFSMethod *method,
GnomeVFSURI *uri,
GnomeVFSFileInfo *file_info,
GnomeVFSFileInfoOptions options,
GnomeVFSContext *context)
{
FakeNode *file;
file = get_fake_node_from_uri (uri);
if (!file) {
return GNOME_VFS_ERROR_NOT_FOUND;
}
if (file->gnode == root) {
/* Root directory. */
file_info->name = g_strdup ("Tutorial");
} else {
file_info->name = g_strdup (file->name);
}
if (file->directory) {
file_info->type = GNOME_VFS_FILE_TYPE_DIRECTORY;
file_info->valid_fields |= GNOME_VFS_FILE_INFO_FIELDS_TYPE;
file_info->mime_type = g_strdup ("x-directory/normal");
file_info->valid_fields |= GNOME_VFS_FILE_INFO_FIELDS_MIME_TYPE;
} else {
file_info->type = GNOME_VFS_FILE_TYPE_REGULAR;
file_info->valid_fields |= GNOME_VFS_FILE_INFO_FIELDS_TYPE;
file_info->mime_type = g_strdup ("text/plain");
file_info->valid_fields |= GNOME_VFS_FILE_INFO_FIELDS_MIME_TYPE;
file_info->size = file->size;
file_info->valid_fields |= GNOME_VFS_FILE_INFO_FIELDS_SIZE;
}
return GNOME_VFS_OK;
}
|
最后一个函数
do_get_file_info 用于获得文件的属性,如文件大小、权限、创建时间、MIME类型等。与读取目录时一样,可以填入我们希望填入的任何数量的信息。例如,可以只设置文件名、MIME类型和文件大小。注意
valid_fields 变量,它的设置值可以让调用者知道填入了哪些字段。
试验
现在我们可以对这个模块进行一些实际的测试了!首先我们需要编译和安装这个模块。完成这项工作的最容易的方式是,将它与 GNOME 安装在的同一前缀中($prefix/lib/gnome-vfs-2.0/modules)。
为了在函数、“tutorial:”和我们的模块之间建立起映射,需要(在$prefix/etc/gnome-vfs-2.0/modules中)安装一个名为
tutorial-method.conf 的简单配置文件,它有下面一行内容:
tutorial: tutorial-method
如果用
make install-1
安装第一个例子,那么所提供的 makefile 应该能安装这个文件(见
参考资料
中到 makefile 和其他在本文中使用的有用源代码的链接)。
如果不希望 -- 或者不能 -- 在系统目录中安装这个模块,那么参见在上述源代码包中的 README 文件中有关如何在主目录中安装模块的指导。
可以在 GnomeVFS 源代码中找到一个调试和测试 VFS模块 的很好的工具。如果有源代码(见
参考资料
列表),试一下这个小程序 gnome-vfs/test/test-shell,它是一个命令行 shell,可以让您在 VFS 模块中浏览目录和文件。下面是一个例子:
清单13. 使用test-shell的例子
./test-shell
gnome-vfs/test> cd tutorial:
tutorial:/ > ls
[documents] , type 'x-directory/normal'
test.txt , type 'text/plain'
tutorial:/ > cd documents
tutorial:/documents/ > ls
todo.txt , type 'text/plain'
tutorial:/documents/ > cat todo.txt
Buy milk
Wash dishes
tutorial:/documents/ > quit
|
为了使这个演示更生动,重新启动 Nautilus (中止 nautilus 过程,或者在终端键入
nautilus -q ),然后在地址栏输入
tutorial:。现在您应该可以看到目录和文件了。如果打开一个文件,那么
Nautilus 就会像对一个常规文本文件那样处理它,因为我们在代码中把 MIME 类型设置为文本文件了。
图2. Nautilus显示根目录
如果只是准备在模块中支持读取,那么应该可以从这一点出发并实现它。我们现在展示如何让模块创建和删除文件和目录、以及如何写入文件。
写入和删除文件
首先,我们需要在
do_open 中添加写支持。这其实很简单:我们只需要检查文件是否存在,如果文件不存在,就创建它。回忆清单7中的
do_open 函数,我们将修改它,以增加对写的支持。
清单14.在do_open()中增加内容
if (mode & GNOME_VFS_OPEN_WRITE) {
file = get_fake_node_from_uri (uri);
if (file) {
g_free (file->content);
file->content = NULL;
file->size = 0;
} else {
file = add_fake_node (uri, FALSE);
}
} else if (mode & GNOME_VFS_OPEN_READ) {
|
我们还需要能够创建文件,这意味着我们必须实现
do_create 。
清单15. do_create()
static GnomeVFSResult
do_create (GnomeVFSMethod *method,
GnomeVFSMethodHandle **method_handle,
GnomeVFSURI *uri,
GnomeVFSOpenMode mode,
gboolean exclusive,
guint perm,
GnomeVFSContext *context)
{
/* We cheat here and don't take perm or exclusive into consideration. */
return do_open (method, method_handle, uri, mode, context);
}
|
它与
do_open 的不同在于可以指定在文件上使用的权限、并且有一个
exclusive
参数,当这个参数设置为
TRUE 时,如果文件已经存在则函数会失败。可以在代码中看到,我们没有使用权限,也没有使用
exclusive 参数 ——这是为了使这个例子更容易一些。
下一步是实现
do_write 和
do_unlink 。
清单16. do_write()
static GnomeVFSResult
do_write (GnomeVFSMethod *method,
GnomeVFSMethodHandle *method_handle,
gconstpointer buffer,
GnomeVFSFileSize bytes,
GnomeVFSFileSize *bytes_written,
GnomeVFSContext *context)
{
FileHandle *handle = (FileHandle *) method_handle;
FakeNode *file;
file = handle->fnode;
file->content = g_memdup (buffer, bytes);
file->size = bytes;
*bytes_written = bytes;
return GNOME_VFS_OK;
}
|
对于我们的简单文件系统来说,写是很容易实现的,就是从句柄中取出“伪文件”,并将缓存中的内容拷贝到文件中就行了。
可以看到它是通过在测试 shell 中拷贝文件,然后打开它来完成其工作的。注意您不能在 Nautilus 中查看新创建的文件的内容,因为我们的伪文件系统是“per
process”的,而 Nautilus 中的文件查看程序和其他查看程序 —— 如Gedit,是运行在单独的进程中的。当然,对于真正的模块就不是这种情况了。不要忘记用命令
make install-2 安装我们例子的新版本。
要能够删除文件,需要实现函数
do_unlink。
清单17. do_unlink()
static GnomeVFSResult
do_unlink (GnomeVFSMethod *method,
GnomeVFSURI *uri,
GnomeVFSContext *context)
{
FakeNode *file;
file = get_fake_node_from_uri (uri);
if (!file) {
return GNOME_VFS_ERROR_INVALID_URI;
}
/* Can't remove the root. */
if (file->gnode == root) {
return GNOME_VFS_ERROR_NOT_PERMITTED;
}
/* Can't remove directories. */
if (file->directory) {
return GNOME_VFS_ERROR_NOT_PERMITTED;
}
return remove_fake_node_by_uri (uri);
}
|
删除文件或者取消文件链接也是相当直观的。我们执行几项检查以确保URI指向的是真正的文件,然后从树中删除它。
创建和删除目录
要实现的最后两个函数是
do_make_directory 和
do_remove_directory :
清单18. do_make_directory()
static GnomeVFSResult
do_make_directory (GnomeVFSMethod *method,
GnomeVFSURI *uri,
guint perm,
GnomeVFSContext *context)
{
GnomeVFSResult result;
FakeNode *file;
file = add_fake_node (uri, TRUE);
if (file) {
result = GNOME_VFS_OK;
} else {
result = GNOME_VFS_ERROR_NOT_PERMITTED;
}
return result;
}
|
这里我们试图添加URI,如果成功,就返回
GNOME_VFS_OK 。如果不成功,就返回
GNOME_VFS_ERROR_NOT_PERMITTED ,以表明不能创建这个
uri
指向的目录。
我们的文件系统没有文件权限的概念,所以我们在这里应忽略
perm 参数。
清单19. do_remove_directory()
static GnomeVFSResult
do_remove_directory (GnomeVFSMethod *method,
GnomeVFSURI *uri,
GnomeVFSContext *context)
{
FakeNode *file;
file = get_fake_node_from_uri (uri);
if (!file) {
return GNOME_VFS_ERROR_INVALID_URI;
}
/* Can't remove the root. */
if (file->gnode == root) {
return GNOME_VFS_ERROR_NOT_PERMITTED;
}
/* Can't remove non-empty directories. */
if (g_node_n_children (file->gnode) > 0) {
return GNOME_VFS_ERROR_NOT_PERMITTED;
}
return remove_fake_node_by_uri (uri);
}
|
首先我们检查
uri 是否存在。如果它不存在,那么我们就返回
GNOME_VFS_ERROR_INVALID_URI ,否则我们检查
URI 是否指向根节点,用户是不允许删除这个节点的。我们还限制删除非空目录。
要考虑的事项
线程安全性
如果在 Nuatilus 中用“tutorial:”位置做练习,移动、创建和删除文件,那么您可能注意到很容易让 Nautilus 崩溃。这是因为我们没有注意到线程安全性。就是说,我们没有保护我们的数据当前不被读取和写入。
如果有任何类型的数据可以在同一时刻被多次访问,比如我们的伪文件树,那么必须确保每一个访问都由线程 mutexe 或者 lock 保护。可以用
GLib 的线程相关功能完成这项工作,例如:
清单20. 使模块具有线程安全性
G_LOCK_DEFINE_STATIC (root);
static GNode *root = NULL;
/* And then protect accesses to the tree like this: */
G_LOCK (root);
...
G_UNLOCK (root);
|
取消操作
在编写 GnomeVFS 模块时,一种常见的情况是文件是真正的文件,而不是像在我们例子中的伪文件。操作可能花很长时间,因此必须有办法让用户取消这些操作。为了避免这种情况,有一些函数可以使用,使得在无需大量额外工作的情况下就可以取消自己的函数。这些函数是在
libgnomevfs/gnome-vfs-cancellable-ops.h 中定义的,并且应该是十分直观的。还可以在 GnomeVFS
中的 API 文档中了解更多有关知识。
在我们的例子中,我们没有让函数成为可取消的,因为文件是从内存中读取的,它非常快。
尾声
编写一个基本的 GnomeVFS 模块并不是很困难的。困难的部分是正确地使用所有的语法,使它的行为就像一个真正的文件系统。如果正在编写一个模块,并将它当成一个真正文件系统公开给用户,我们鼓励您将一些精力花在细节上,保证其正确,这包括线程安全性。同样,在开始编写模块之前,要再三考虑,确保它对于所要解决的问题是正确的方法。
在最后的例子中,我们为文件系统树的访问增加了锁定功能。可以用
make install-final 试一试它。得到的结果是不会使
Nautilus 崩溃的稳定模块。
结束语
我们用一个内存中的包含几个文件的目录树创建了一个简单的 GnomeVFS 模块。我们还用一个很不错的小型测试 shell 工具以及在 Nautilus
中对这个模块进行了测试。现在,您应该可以实现自己的模块了。但是就像每一个开发项目一样,在确定 GnomeVFS 模块真的就是完成当前任务的正确的方法之前,请花一些时间和精力进行计划。
参考资料
作者简介  | |  | Mikael Hallendal 和 Richard Hult 最近创立了
Imendio
公司。Mikael 做过三年多的 GNOME 开发人员,是 GNOME 2 中帮助浏览器 Yelp 的作者。他与 Richard 一起开发不同的
GNOME 项目,比如
Devhelp、Gossip (这是一个 GNOME 2 的 Jabber 客户端)和
Loudmouth
(这是一个 Jabber 客户机库)。
|
 | |  | Richard Hult 是
DrWright
的作者,这是一个使用计算机时提醒您休息的非常好的应用程序。目前,他与 Mikael 一道致力于 Gossip 的发布。
|
对本文的评价
|  |