試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

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向けツールMummlGNU/Linux向けの代替を目指して作成したものとなっている。

関連記事:

使用したバージョン:

  • Python 2.6.4
  • PyGTK 2.16.0
  • ppmck 9a / MagicKit 2.51

*1:設定ファイルに保存したりはしていない