The details of Nautilus Renames (TAKE 3).

前回の続き。で、まずは
コマンドラインの実装:
この Nautilus 拡張の大部分の機能、すなわち指定したファイルやフォルダの名前を編集する機能はコマンドラインのプログラムで提供している。名前を編集する際のオプションも指定できるように GUI を備えている。ここで編集対象となるファイルやフォルダは、コマンドラインの引数として受け取るようにしている。GUI はダイアログ形式で、編集した名前に [変更する(R)] と編集そのものを [キャンセル(C)] するというボタンを用意してあり、このいずれかをクリックすると処理を完了しダイアログを消して終了する。
コマンドライン・オプションは次のとおり:

% nautilus-renames --help
用法:
nautilus-renames [オプション...] [ファイル...]
ヘルプのオプション: -?, --help ヘルプのオプションを表示する --help-all ヘルプのオプションを全て表示する --help-gtk GTK+ のオプションを表示する
アプリケーションのオプション: -j, --just-directory フォルダの中身ではなくフォルダ自身の名前を変更する --legend テンプレートで使用可能な特殊文字の凡例を表示する --use-gconf GConf 設定サーバ経由でオプションを利用する --display=DISPLAY 使用するXのディスプレイを指定する

こんな感じなので、Nautilus 拡張機能からは Nautilus ファイル・マネージャ上で選択したファイルやフォルダの並びを引数にして、このコマンドを起動するだけである。
設計段階では、コマンドラインだし GUI で設定したオプション項目の再利用は必要ないだろうと考えていたのだけれど、Nautilus 拡張機能経由で実行する際はやっぱりあった方が便利だなということで GConf 対応した (ほとんど後付けな実装)。そういう経緯で –use-gconf なんて (一見、不要とも思える) オプションがついている。
必須の引数であるファイルの書式としては

  • 通常の UNIX ファイル名 (例: /home/aihana/photos/0007.jpg)
  • URI 形式の名前 (例: file:///home/aihana/photos/0007.jpg)

の二つをサポートしている。ファイルの他にフォルダを指定することも可能で、デフォルトではそのフォルダの中にある全てのファイルが変更対象になる。但し、–just-directory オプションを指定した場合はフォルダそのものが変換対象になる。
ファイルやフォルダを指定しないとコマンドラインのレベルでエラーを返す:

$ nautilus-renames
オプションとして URI または ファイル名を指定して下さい

但し、指定したファイルやフォルダが一つでも実在していなければダイアログを出す:
nautilus-renames-not-found-20080710.png
nautilus-renames コマンドラインの実装は次のファイルから構成されている:

nautilus-renames
|-- m4
|-- po
`-- src
|-- nautilus-renames-command.c (これが main でオプションの処理とか)
|-- nautilus-renames-dialog.c (これが GUI の処理)
|-- mkf-file-data.[c,h] (ファイル情報を扱う独自のデータ型とその処理)
|-- mkf-file-utils.[c,h] (ファイル関連のユーティリティ関数)
`-- mkf-debug.[c,h] (デバッグ出力用の関数)

nautilus-renames-command.c
このソースに nautilus-renames コマンドの main() がある。やっていることはというと:

  1. i18n 処理の初期化
  2. Threads の初期化
  3. 専用のデバッグ関数の初期化
  4. コマンドライン・オプションの解析
  5. GNOME-VFS の初期化
  6. 引数のファイル名を URI 形式に一括変換
  7. ファイル名変更ダイアログを表示
  8. gtk_main() でイベント待ち
  9. 終了処理

「i18n 処理の初期化」はプログラムの中でこれを使うための初期化を config.h 経由で行っているいつもの Boilerplate Codes
「Threads の初期化」では、Nautilus の拡張機能で複数のスレッドから GLib の関数にアクセスする可能性があるので、一応おまじないとして。必ず GLib の関数を呼ぶ前に実施する

/* Threads must be initialized before calling ANY glib functions */
g_thread_init (NULL);

「専用のデバッグ関数の初期化」はオリジナルのデバッグ・メッセージを出力する関数を利用するために環境変数 MKF_DEBUG を取得したり、デバッグ・メッセージ毎に処理時間を出力するためのタイマーを初期化したり。これは mkf-debug.h でエキスポートしている関数を呼び出すだけ。
「コマンドライン・オプションの解析」はもちろん GOption* を使った解析で、ほぼこれも Boilerplate Codes:

GOptionContext *context;
(..snip..)
context = g_option_context_new (N_("[FILES...]")); g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); g_option_context_add_main_entries (context, option_entries, GETTEXT_PACKAGE); g_option_context_add_group (context, gtk_get_option_group (TRUE)); if (g_option_context_parse (context, &argc, &argv, &error) == FALSE) { g_print (_("Could not parse command-line options: %s\n"), error->message); g_error_free (error); return 1; } g_option_context_free (context);

GOption* を表現するコンテキストを生成し、お好みのオプション・リストを GOptionEntry 型で定義しておき、コンテキストにセットしてやる:

static const GOptionEntry option_entries[] =
{
{ "just-directory", 'j', 0, G_OPTION_ARG_NONE, &just_directory,
N_("Rename directory itself instead of contents in it"), NULL },
{ "legend", 0, 0, G_OPTION_ARG_NONE, &show_legend,
N_("Show legend to be able to use in the template"), NULL },
{ "use-gconf", 0, 0, G_OPTION_ARG_NONE, &use_gconf,
N_("Use options via GConf configuration server"), NULL },
/* Remains in arguments */
{ G_OPTION_REMAINING, '', 0, G_OPTION_ARG_FILENAME_ARRAY, &pathnames, N_("Movies to index"), NULL },
{ NULL }
};

さらにオプションをグループ化して見やすくしてから、実際に受け取った引数を解析する。解析結果は予め定義しておいたグローバル変数に格納される:

/* Command-line options */
static gboolean just_directory = FALSE;
static gboolean show_legend    = FALSE;
static gboolean use_gconf      = FALSE;
static char   **pathnames      = NULL;

GOption で i18n の機能を有効にするのであれば

g_option_context_set_translation_domain (context, GETTEXT_PACKAGE);

は忘れない方がよい (案外、忘れる)。解析が終わったら使用したコンテキストを解放しておく。
これ以降はオプションに応じた処理になり、引数としてファイル名が指定されていなければ、前述ようなエラー・メッセージを表示して 1 で終了する。ファイル名が指定されていたら、次は gnome_vfs_init() を呼び出して GNOME-VFS の各種ユーティリティを利用できるようにしておく。
実装当初、ファイル名の変更そのものの処理は g_spwan_async() とか GLib のファイル・ユーティリティ関数なんかを使って UNIX コマンドで実行すればよいか程度に思っていたけど、それじゃあつまらないので Nautilus の実装に合わせて GNOME の仮想ファイルシステム経由で実施することにした。このコマンドは Nautilus 拡張機能だから新たにインストールする必要もないし。それに、将来は Nautilus 拡張機能のデフォルト・プロバイダ経由で (外部コマンドを起動せずに) ファイル名を変更できるかもしれないし。
「引数のファイル名を URI 形式に一括変換」では GNOME-VFS で処理しやすくするために全てのファイルを URI 形式に変更して GList 型のリストに格納する。変更する際は指定したファイルが実際に存在しているかテストしておき、存在していなければリストには格納しない。
リストの格納方法は、まず一つ一つ

list = g_list_prepend (list, fd);

して、最後に一気に

fd_list = g_list_reverse (fd_list);

するのが常道。
このリストに格納するのはオリジナルの FileData という簡単なデータ型:

typedef struct {
gchar       *uri;              /* Full pathname with URI scheme */
const gchar *name;             /* Filename only */
gchar       *display_name;     /* Filename displayed on GUI */
const gchar *mime_type;
goffset      size;
time_t       ctime;
time_t       mtime;
} FileData;

で、実装は src/mkf-file-data.[c,h] にある。ちょっと補足しておくと、メンバの uri はここで変換した URI のことで完全な絶対パスになっており、その下の name はディレクトリを除いたファイル名 (システム内部表現) であり、次の display_name はダイアログに表示する際の name のことで、UTF-8 エンコーディングが施された文字列である (日本語のファイル名をダイアログにひょうじするため)。残りのメンバは、ファイル名を編集する上で必要となるパラメータである。
それから、もしリストが空ならば前述の GtkMessageDialog を表示し 1 で終了する。この一時的なメッセージ・ダイアログもよく利用するコードの集合:

if (fd_list == NULL) {
GtkWidget *error_dialog;
error_dialog = gtk_message_dialog_new_with_markup (NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, _("<big><b>Failed to rename target files</b><big>")); gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (error_dialog), _("It looks specified URIs or filenames are not exist actually. " "Please check your targets.")); gtk_window_set_title (GTK_WINDOW (error_dialog), ""); gtk_container_set_border_width (GTK_CONTAINER (error_dialog), 5); gtk_dialog_set_default_response (GTK_DIALOG (error_dialog), GTK_RESPONSE_CLOSE); gtk_dialog_run (GTK_DIALOG (error_dialog)); return 1; }

ここまででファイル名を変更する材料が揃ってきたので、GTK+ 製のダイアログで名前の付け方を調整し実際に変更するわけで、GUI 関連は全て nautilus-renames-dialog.cnautilus-renames-dialog.glade の方で行っている。ここではファイル名編集ダイアログを gtk_widget_show() して戻ってくるので、あとはメイン・ループに入ってイベントを待つ。
ファイル名を変更するかキャンセルするか、もしくはウィンドウで gtk_main_quit() すると次の「終了処理」にくるので、今まで利用してきたリストなどを解放し 0 で終了する。
ここまでが nautilus-renames コマンドの大きな流れ。次回は nautilus-renames-dialog.c と GNOME-VFS の API について説明する予定。
このブログを書くのに4日もかかった :o