MMLファイルの変更を監視して上書き保存時にppmck(ppmckc/nesasm)でNSF変換を自動で行うツールを更新(2010/2/17版)
「GNU/Linux上におけるppmckについてのその後(2010/2/2現在)」の内容を受けて、MMLファイルの変更を監視して上書き保存時にppmck(ppmckc/nesasm)でNSF変換を自動で行うツールを大幅に手直しし、GNU/Linux向けにビルドしたppmckc,nesasmの両コマンドとppmckのnes_includeディレクトリ以下を利用してNSF変換を行うようにした。処理の流れもmknsfのスクリプトのようにシンプルにしている(中間ファイルを生成する関係で、変換自体は一時ディレクトリ内で行っている)。
また、変換処理の外部プロセスの出力はGUIのテキストビュー上に表示するようにしている。
コード
PyGTKとGNU/Linux用にビルドされたppmck(nesasm含む)が入っていれば、このスクリプト単独で動作する。「.mmlファイルを監視して上書き保存時に Wine上のppmckで.mmlファイルから.nsfファイルへ自動で変換するためのGUIツールを作成(2009/12/17版)」のときとは異なり変換用スクリプトは別にはなっていない。
実行ファイルなどの場所の初期値は現状では固定となっている*1他、細かい部分で不完全な部分は色々とあるかも知れない。
[任意]ファイル名: mmlwatchergtk-20100331.py ライセンス: GPL-3 (or lator)
#! /usr/bin/python # -*- coding: utf-8 -*- # mmlwatchergtk.py for ppmck 20100331 # (C) 2010 kakurasan # convert MML file automatically when updated # Licensed under GPLv3+ import tempfile import shutil import urllib import ctypes import time import sys import re import os try: import pygtk pygtk.require('2.0') except: pass try: import pango import gtk except: print >> sys.stderr, 'Error: PyGTK is not installed' sys.exit(1) try: from glib import timeout_add_seconds as glib_timeout_add_seconds from glib import source_remove as glib_source_remove from glib import spawn_async as glib_spawn_async from glib import child_watch_add as glib_child_watch_add from glib import SPAWN_DO_NOT_REAP_CHILD as glib_SPAWN_DO_NOT_REAP_CHILD from glib import GError as glib_GError from glib import io_add_watch as glib_io_add_watch from glib import IO_IN as glib_IO_IN except: try: from gobject import timeout_add_seconds as glib_timeout_add_seconds from gobject import source_remove as glib_source_remove from gobject import spawn_async as glib_spawn_async from gobject import child_watch_add as glib_child_watch_add from gobject import SPAWN_DO_NOT_REAP_CHILD as glib_SPAWN_DO_NOT_REAP_CHILD from gobject import GError as glib_GError from gobject import io_add_watch as glib_io_add_watch from gobject import IO_IN as glib_IO_IN except: print >> sys.stderr, 'Error: cannot import GLib functions' sys.exit(1) class RegExPatterns: """ RegEx pattern objects """ dpcm_relpath = re.compile('( *@DPCM[0-9]+ *= *{")([^"]+)(".+)') class LabelWithMnemonic(gtk.Label): """ label with mnemonic widget """ def __init__(self, str, widget): gtk.Label.__init__(self, str) self.props.use_underline = True self.props.mnemonic_widget = widget class BrowseButton(gtk.Button): """ 'browse' button """ def __init__(self, entry, dir=False): gtk.Button.__init__(self) self.__entry = entry self.__dir = dir self.props.label = '...' self.__filename = None self.connect('clicked', self.__on_self_clicked) def set_filename(self, filename): self.__filename = filename def __on_self_clicked(self, widget): if self.__dir: opendlg = gtk.FileChooserDialog(title='Select dir', action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT)) else: opendlg = gtk.FileChooserDialog(title='Select file', buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT)) opendlg.props.local_only = True if self.__filename: opendlg.set_filename(self.__filename) if opendlg.run() == gtk.RESPONSE_ACCEPT: self.__filename = opendlg.get_filename() self.__entry.props.text = self.__filename opendlg.destroy() class PathEntry(gtk.Entry): """ entry for path """ __TARGET_TYPE_TEXT_URI_LIST = 12345 __dnd_list = [('text/uri-list', 0, __TARGET_TYPE_TEXT_URI_LIST),] def __init__(self): gtk.Entry.__init__(self) self.connect('drag_data_received', self.__on_drag_data_received) self.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP, self.__dnd_list, gtk.gdk.ACTION_COPY) def __on_drag_data_received(self, widget, context, x, y, selection, info, time): """ set text to received path """ for item in selection.data.rstrip('\x00').splitlines(): if item.startswith('file:'): path = os.path.normpath(urllib.url2pathname(item[5:])) if os.access(path, os.R_OK): self.props.text = path class MainWindow(gtk.Window): """ application main window """ def __init__(self, path, *args, **kwargs): gtk.Window.__init__(self, *args, **kwargs) # accelgroup self.__accelgroup = gtk.AccelGroup() self.add_accel_group(self.__accelgroup) # menuitems self.__item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.__accelgroup) self.__menu_file = gtk.Menu() self.__menu_file.add(self.__item_quit) self.__item_file = gtk.MenuItem('_File') self.__item_file.props.submenu = self.__menu_file self.__menubar = gtk.MenuBar() self.__menubar.append(self.__item_file) # entry for path to ppmckc self.__entry_ppmckc = PathEntry() # entry for path to nesasm self.__entry_nesasm = PathEntry() # entry for path to player self.__entry_player = PathEntry() # entry for path to nesinc self.__entry_nesinc = PathEntry() # entry for path to outdir self.__entry_outdir = PathEntry() # entry for path to input file self.__entry_infile = PathEntry() # browse buttons self.__btn_browse_ppmckc = BrowseButton(self.__entry_ppmckc) self.__btn_browse_nesasm = BrowseButton(self.__entry_nesasm) self.__btn_browse_nesinc = BrowseButton(self.__entry_nesinc, dir=True) self.__btn_browse_player = BrowseButton(self.__entry_player) self.__btn_browse_outdir = BrowseButton(self.__entry_outdir, dir=True) self.__btn_browse_infile = BrowseButton(self.__entry_infile) # log font self.__fontbtn = gtk.FontButton('Monospace 8') # labels self.__label_ppmckc = LabelWithMnemonic('_ppmckc:', self.__entry_ppmckc) self.__label_nesasm = LabelWithMnemonic('_nesasm:', self.__entry_nesasm) self.__label_nesinc = LabelWithMnemonic('nes _include:', self.__entry_nesinc) self.__label_player = LabelWithMnemonic('p_layer:', self.__entry_player) self.__label_outdir = LabelWithMnemonic('_output directory:', self.__entry_outdir) self.__label_infile = LabelWithMnemonic('_MML file:', self.__entry_infile) # start/stop watching self.__btn_watch = gtk.ToggleButton('_Watch') self.__btn_watch.props.sensitive = False # message self.__textbuf = gtk.TextBuffer() self.__textview = gtk.TextView(self.__textbuf) self.__textview.props.editable = False self.__textview.modify_font(pango.FontDescription('Monospace 8')) self.__sw = gtk.ScrolledWindow() self.__sw.add(self.__textview) self.__sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) # containers self.__table = gtk.Table(6, 3) self.__table.attach(self.__label_ppmckc, 0, 1, 0, 1) self.__table.attach(self.__entry_ppmckc, 1, 2, 0, 1) self.__table.attach(self.__btn_browse_ppmckc, 2, 3, 0, 1) self.__table.attach(self.__label_nesasm, 0, 1, 1, 2) self.__table.attach(self.__entry_nesasm, 1, 2, 1, 2) self.__table.attach(self.__btn_browse_nesasm, 2, 3, 1, 2) self.__table.attach(self.__label_nesinc, 0, 1, 2, 3) self.__table.attach(self.__entry_nesinc, 1, 2, 2, 3) self.__table.attach(self.__btn_browse_nesinc, 2, 3, 2, 3) self.__table.attach(self.__label_player, 0, 1, 3, 4) self.__table.attach(self.__entry_player, 1, 2, 3, 4) self.__table.attach(self.__btn_browse_player, 2, 3, 3, 4) self.__table.attach(self.__label_outdir, 0, 1, 4, 5) self.__table.attach(self.__entry_outdir, 1, 2, 4, 5) self.__table.attach(self.__btn_browse_outdir, 2, 3, 4, 5) self.__table.attach(self.__label_infile, 0, 1, 5, 6) self.__table.attach(self.__entry_infile, 1, 2, 5, 6) self.__table.attach(self.__btn_browse_infile, 2, 3, 5, 6) self.__hbox = gtk.HBox() self.__hbox.pack_start(self.__table) self.__hbox.pack_start(self.__btn_watch) self.__vbox = gtk.VBox() self.__vbox.pack_start(self.__menubar, expand=False, fill=False) self.__vbox.pack_start(self.__hbox, expand=False, fill=False) self.__vbox.pack_start(self.__sw) self.__vbox.pack_start(self.__fontbtn, expand=False, fill=False) # signals self.connect('delete_event', gtk.main_quit) self.connect('drag_data_received', self.__on_drag_data_received) self.__item_quit.connect('activate', gtk.main_quit) self.__entry_infile.connect('changed', self.__on_entry_changed) self.__fontbtn.connect('font-set', lambda widget: self.__textview.modify_font(pango.FontDescription(widget.props.font_name))) self.__id_btnsig = self.__btn_watch.connect('toggled', self.__on_button_watch_toggled) self.__id_watch = None # window self.props.title = 'MMLWatcherGTK 20100331' self.add(self.__vbox) self.set_size_request(400, 400) # dnd self.__TARGET_TYPE_TEXT_URI_LIST = 23456 self.__dnd_list = [('text/uri-list', 0, self.__TARGET_TYPE_TEXT_URI_LIST),] self.__drag_set_acceptable(True) # GLib self.glib = ctypes.cdll.LoadLibrary('libglib-2.0.so.0') # default path (FOR TESTING) self.__entry_ppmckc.props.text = '/usr/bin/ppmckc' self.__entry_nesasm.props.text = '/usr/bin/nesasm' self.__entry_nesinc.props.text = '/usr/share/ppmck/nes_include' try: self.__entry_outdir.props.text = os.environ['HOME'] except: self.__entry_outdir.props.text = '/tmp' self.__entry_player.props.text = '/usr/bin/mednafen' # path from argv if path: try: f = open(path, 'r') f.close() self.__btn_browse_infile.set_filename(path) self.__entry_infile.props.text = path self.__btn_watch.props.active = True except: self.__append_text_and_scroll('Warning: file "%s" not found' % path) def __on_entry_changed(self, widget): """ file path changed """ self.__btn_watch.props.sensitive = os.access(widget.props.text, os.R_OK) def __on_button_watch_toggled(self, widget): """ toggled 'watch' button """ if widget.props.active: # check nes_include nesinc = self.__entry_nesinc.props.text found_ppmck = False found_ppmck_asm = False try: for item in os.listdir(nesinc): if item == 'ppmck': found_ppmck = True elif item == 'ppmck.asm': found_ppmck_asm = True except OSError, (errno, msg): self.__append_text_and_scroll('Error in nes include: %s\n' % msg) # block handler and set 'inactive' self.__btn_watch.handler_block(self.__id_btnsig) self.__btn_watch.props.active = False self.__btn_watch.handler_unblock(self.__id_btnsig) return if found_ppmck == False or found_ppmck_asm == False: self.__append_text_and_scroll('Error: not nes_include directory\n') self.__btn_watch.handler_block(self.__id_btnsig) self.__btn_watch.props.active = False self.__btn_watch.handler_unblock(self.__id_btnsig) return # set NES_INCLUDE os.environ['NES_INCLUDE'] = nesinc # check ppmckc/nesasm try: glib_spawn_async([self.__entry_ppmckc.props.text,], standard_output=True) except glib_GError, (msg): self.__append_text_and_scroll('Error: Could not execute ppmckc: %s\n' % msg) self.__btn_watch.handler_block(self.__id_btnsig) self.__btn_watch.props.active = False self.__btn_watch.handler_unblock(self.__id_btnsig) return # start watching try: self.__mtime = os.path.getmtime(self.__entry_infile.props.text) except OSError, (errno, msg): self.__append_text_and_scroll('Error: cannot stat "%s": %s\n' % (self.__entry_infile.props.text, msg)) self.__btn_watch.handler_block(self.__id_btnsig) self.__btn_watch.props.active = False self.__btn_watch.handler_unblock(self.__id_btnsig) return for widget in [self.__entry_ppmckc, self.__btn_browse_ppmckc, self.__entry_nesasm, self.__btn_browse_nesasm, self.__entry_nesinc, self.__btn_browse_nesinc, self.__entry_player, self.__btn_browse_player, self.__entry_outdir, self.__btn_browse_outdir, self.__entry_infile, self.__btn_browse_infile]: widget.props.sensitive = False self.__id_watch = glib_timeout_add_seconds(1, self.__to_watch_file) self.__append_text_and_scroll('start watching "%s" (%s)\n' % (self.__entry_infile.props.text, time.ctime(None))) else: # stop watching if self.__id_watch: glib_source_remove(self.__id_watch) self.__id_watch = None self.__append_text_and_scroll('stop watching "%s"\n' % self.__entry_infile.props.text) for widget in [self.__entry_ppmckc, self.__btn_browse_ppmckc, self.__entry_nesasm, self.__btn_browse_nesasm, self.__entry_nesinc, self.__btn_browse_nesinc, self.__entry_player, self.__btn_browse_player, self.__entry_outdir, self.__btn_browse_outdir, self.__entry_infile, self.__btn_browse_infile]: widget.props.sensitive = True def __append_text_and_scroll(self, text): """ append text (TextBuffer) and scroll (TextView) """ self.__textbuf.place_cursor(self.__textbuf.get_end_iter()) self.__textbuf.insert_at_cursor(text) self.__textview.scroll_to_mark(self.__textbuf.get_insert(), 0) def __to_watch_file(self): """ watch timestamp """ try: new_mtime = os.path.getmtime(self.__entry_infile.props.text) except OSError, (errno, msg): self.__append_text_and_scroll('Error: cannot stat "%s": %s\n' % (self.__entry_infile.props.text, msg)) self.__btn_watch.props.active = False return False # check timestamp if self.__mtime != new_mtime: self.__mtime = new_mtime # store new timestamp # file is updated self.__append_text_and_scroll('file "%s" updated (%s)\n' % (self.__entry_infile.props.text, time.ctime(self.__mtime))) self.__drag_set_acceptable(False) # make tempdir self.__tempdir = tempfile.mkdtemp('mmlwatchergtk.') # input -> tempdir infile = self.__entry_infile.props.text infile_basename = os.path.basename(infile) infile_dirname = os.path.dirname(infile) os.chdir(infile_dirname) try: f_in = open(infile, 'r') except IOError, (errno, msg): self.__append_text_and_scroll('Error: cannot open "%s": %s\n' % (infile, msg)) return True try: f_out = open(os.path.join(self.__tempdir, infile_basename), 'w') except IOError, (errno, msg): self.__append_text_and_scroll('Error: cannot open "%s" (in tempdir): %s\n' % (infile_basename, msg)) f_in.close() return True try: try: for l in f_in: # DPCM rel.path -> abs.path m = RegExPatterns.dpcm_relpath.search(l) if m: l = RegExPatterns.dpcm_relpath.sub(r'\1', l).strip() + \ os.path.abspath(RegExPatterns.dpcm_relpath.sub(r'\2', l)).strip() + \ RegExPatterns.dpcm_relpath.sub(r'\3', l) f_out.write(l) except IOError, (errno, msg): self.__append_text_and_scroll('Error: cannot write to file "%s" (in tempdir): %s\n' % (infile_basename, msg)) return True finally: f_in.close() f_out.close() # enter tempdir os.chdir(self.__tempdir) # spawn ppmckc (pid_ppmckc, fd_stdin_ppmckc, fd_stdout_ppmckc, fd_stderr_ppmckc) = glib_spawn_async([self.__entry_ppmckc.props.text, '-i', infile_basename], flags=glib_SPAWN_DO_NOT_REAP_CHILD, standard_output=True) glib_io_add_watch(fd_stdout_ppmckc, glib_IO_IN, self.__on_iowatch_in) self.__id_childwatch_ppmckc = glib_child_watch_add(pid_ppmckc, self.__on_ppmckc_done) return True; def __on_ppmckc_done(self, pid, status): """ ppmckc exited """ # close pid self.glib.g_spawn_close_pid(pid) # remove event source for childwatch glib_source_remove(self.__id_childwatch_ppmckc) # check status if os.WIFEXITED(status): exitstatus = os.WEXITSTATUS(status) if exitstatus == 0: self.__append_text_and_scroll('Info: compiled "%s"\n' % self.__entry_infile.props.text) # check effect.h try: f = open('effect.h', 'r') f.close() except: self.__append_text_and_scroll('Error: effect.h not found!\n') self.__drag_set_acceptable(True) return # spawn nesasm (pid_nesasm, fd_stdin_nesasm, fd_stdout_nesasm, fd_stderr_nesasm) = glib_spawn_async([self.__entry_nesasm.props.text, '-s', '-raw', 'ppmck.asm'], flags=glib_SPAWN_DO_NOT_REAP_CHILD, standard_output=True) glib_io_add_watch(fd_stdout_nesasm, glib_IO_IN, self.__on_iowatch_in) self.__id_childwatch_nesasm = glib_child_watch_add(pid_nesasm, self.__on_nesasm_done) else: self.__append_text_and_scroll('Error: ppmckc failed (exit status=%d)' % exitstatus) self.__drag_set_acceptable(True) else: self.__append_text_and_scroll('Error: ppmckc not exited (status=%d)' % status) self.__drag_set_acceptable(True) def __on_nesasm_done(self, pid, status): """ nesasm exited """ # close pid self.glib.g_spawn_close_pid(pid) # remove event source for childwatch glib_source_remove(self.__id_childwatch_ppmckc) # check status success = False if os.WIFEXITED(status): if os.WEXITSTATUS(status) == 0: self.__append_text_and_scroll('Info: assembled "%s"\n' % os.path.join(self.__entry_nesinc.props.text, 'ppmck.asm')) # copy NSF file into output directory outfile = os.path.join(self.__entry_outdir.props.text, '%s.nsf' % os.path.basename(self.__entry_infile.props.text)[:-4]) # [:-4]: strip '.mml' try: shutil.copy2('ppmck.nes', outfile) self.__append_text_and_scroll('Info: wrote "%s" (%s)\n' % (outfile, time.ctime(None))) success = True except IOError, (errno, msg): self.__append_text_and_scroll('Error: copy failed: %s\n' % msg) else: print self.__append_text_and_scroll('Error: assemble failed\n') # cleanup tempdir os.chdir('/') shutil.rmtree(self.__tempdir) # dnd self.__drag_set_acceptable(True) # player if success: try: (pid_player, fd_stdin_player, fd_stdout_player, fd_stderr_player) = glib_spawn_async([self.__entry_player.props.text, outfile], standard_output=True) glib_io_add_watch(fd_stdout_player, glib_IO_IN, self.__on_iowatch_in) except: pass # ignore exception def __on_iowatch_in(self, fd, condition): """ read child output and insert into TextBuffer """ f = os.fdopen(fd, 'r') for l in f: self.__append_text_and_scroll(l) f.close() return False def __drag_set_acceptable(self, acceptable): """ set acceptable status """ if acceptable == True: self.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP, self.__dnd_list, gtk.gdk.ACTION_COPY) else: self.drag_dest_unset() def __on_drag_data_received(self, widget, context, x, y, selection, info, time): """ received drag data """ item = selection.data.rstrip('\x00').splitlines()[-1] # use only last one if item.startswith('file:'): path = os.path.normpath(urllib.url2pathname(item[5:])) # 5=len('file:') if self.__id_watch: # now watching self.__btn_watch.props.active = False # stop watching self.__entry_infile.props.text = path # update entry self.__btn_watch.props.active = True # start watching class MMLWatcherGTK: """ MML watcher for ppmck/nesasm """ def main(self): """ main """ path = None if len(sys.argv) > 1: path = sys.argv[1] win = MainWindow(path) win.show_all() gtk.main() if __name__ == '__main__': app = MMLWatcherGTK() app.main()
(2010/2/27)以下の点を改善した。
- DPCMを使用したときにDMCファイルの場所をMMLファイルからの相対パスで記述している場合、一時ディレクトリへのコピー時にこれを絶対パス化して正しく読み込めるようにした
- コマンド行引数でファイルを指定したときの動作がおかしかったのを修正
- コード全体においてGObjectプロパティを用いた書き方に変更
- その他微調整
(2010/3/31)フォント変更の処理を調整
使い方
をそれぞれ絶対パスで指定し、「Watch」ボタンを押す。MMLファイルが変更されると自動的に変換作業が行われ出力ディレクトリ内にNSFファイルが保存される。
MMLファイルを編集するテキストエディタについては任意だが、「Wine上のサクラエディタの動作について(バージョン1.6.5.0時点)」のようにしてWine上のサクラエディタを使うこともできる。
http://wikiwiki.jp/mck/?%C0%A9%BA%EE%B4%C4%B6%AD%A4%CE%BD%E0%C8%F7
も参照。なお、本ツールはそのページに書かれているWindows向けツールMummlのGNU/Linux向けの代替を目指して作成したものとなっている。
関連記事:
- ショートカットキーにより、テキストラベルに関連付けた別のウィジェットをフォーカスする
- Pythonでファイルの変更を監視する(OS非依存・PyGObject使用)
- .mmlファイルを監視して上書き保存時に Wine上のppmckで.mmlファイルから.nsfファイルへ自動で変換するためのGUIツールを作成(2009/12/17版)
- GNU/Linux上におけるppmckについてのその後(2010/2/2現在)
- PyGTKでフォント選択ボタンのフォントをテキストビューに適用
- Wine上のサクラエディタの動作について(バージョン1.6.5.0時点)
使用したバージョン:
- Python 2.6.4
- PyGTK 2.16.0
- ppmck 9a / MagicKit 2.51
*1:設定ファイルに保存したりはしていない