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

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

unified形式のdiffを取得してGTK+のテキストビューに表示する(diffファイル保存可)

Pythonのdifflibモジュールを用いて複数行テキストどうしの差分を取得する」の例と同じようにunified形式の差分を取得するプログラムをPyGTKで作成した。
上部の2つのテキスト入力欄にそれぞれのテキストファイルの場所を入力後に下部の「Get diff」ボタンを押すことでGTK+のテキストビューにunified形式の差分を色付けして表示し、これをファイル保存ダイアログから保存することもできる。
色付け処理やフォントの変更は「PyGTKでテキストビュー内の文字列にスタイルを適用する」や「PyGTKでフォント選択ボタンのフォントをテキストビューに適用」の要領。行の最初の文字列を見るだけでどのスタイルで修飾するべきかを判別できるため、色付けは簡単にできた。
(2010/9/20)一部の処理を整理して見やすくしたものとその手法についてを「PyGTKでテキストビューを用いる際にテキストタグを多く作成・設定する場合の処理の整理について」で扱っている。
[任意]ファイル名: getdifftest.py

#! /usr/bin/python
# -*- coding: utf-8 -*-

import difflib
import urllib
import time
import sys
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)


def run_errordialog(title, format1, format2=None):
  """
  エラーダイアログを表示
  """
  errdlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_CLOSE, message_format=format1)
  if format2:
    errdlg.format_secondary_text(format2)
  errdlg.props.title = title
  errdlg.run()
  errdlg.destroy()

def save_difffile(outfile, text):
  """
  差分ファイルを保存
  """
  try:
    f_out = open(outfile, 'w')
  except IOError, (errno, msg):
    run_errordialog('Error', 'cannot open output file "%s": %s', (outfile, msg))
    return False
  try:
    try:
      f_out.write(text)
    except IOError, (errno, msg):
      run_errordialog('Error', 'cannot write output file "%s": %s', (outfile, msg))
      return False
  finally:
    f_out.close()
  return True


class LabelWithMnemonic(gtk.Label):
  """
  ニーモニック・ウィジェットを初期化時に指定するラベル
  """
  def __init__(self, str, widget):
    gtk.Label.__init__(self, str)
    self.props.use_underline = True
    self.props.mnemonic_widget = widget

class BrowseButton(gtk.Button):
  """
  参照ボタン
  """
  def __init__(self, entry):
    gtk.Button.__init__(self)
    self.__entry = entry
    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):
    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):
  """
  パス指定用テキスト入力欄
  """
  __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):
    """
    テキストを受け取ったパスに指定
    """
    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):
  """
  メインウィンドウ
  """
  def __init__(self, *args, **kwargs):
    gtk.Window.__init__(self, *args, **kwargs)
    # ショートカットキー(アクセラレータ)
    self.__accelgroup = gtk.AccelGroup()
    self.add_accel_group(self.__accelgroup)
    # メニュー項目
    self.__item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.__accelgroup)
    self.__item_save = gtk.ImageMenuItem(gtk.STOCK_SAVE, self.__accelgroup)
    self.__item_saveas = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS, self.__accelgroup)
    self.__menu_file = gtk.Menu()
    self.__menu_file.prepend(self.__item_quit)
    self.__menu_file.prepend(gtk.SeparatorMenuItem())
    self.__menu_file.prepend(self.__item_saveas)
    self.__menu_file.prepend(self.__item_save)
    self.__item_file = gtk.MenuItem('_File')
    self.__item_file.props.submenu = self.__menu_file
    self.__menubar = gtk.MenuBar()
    self.__menubar.append(self.__item_file)
    # 入力ファイル
    self.__entry_file_a = PathEntry()
    self.__entry_file_b = PathEntry()
    self.__btn_browse_file_a = BrowseButton(self.__entry_file_a)
    self.__label_file_a = LabelWithMnemonic('_A:', self.__entry_file_a)
    self.__label_file_b = LabelWithMnemonic('_B:', self.__entry_file_b)
    self.__btn_browse_file_b = BrowseButton(self.__entry_file_b)
    # テキストタグ
    self.__texttag_plus3 = gtk.TextTag('plus3')
    self.__texttag_plus3.props.foreground = '#e71585'
    self.__texttag_plus3.props.background = '#ffe4e1'
    self.__texttag_plus3.props.weight = pango.WEIGHT_BOLD
    self.__texttag_plus = gtk.TextTag('plus')
    self.__texttag_plus.props.family = 'Monospace'
    self.__texttag_plus.props.foreground = '#e71585'
    self.__texttag_plus.props.weight = pango.WEIGHT_BOLD
    self.__texttag_minus3 = gtk.TextTag('minus3')
    self.__texttag_minus3.props.foreground = '#4169e1'
    self.__texttag_minus3.props.background = '#e6e6fa'
    self.__texttag_minus3.props.weight = pango.WEIGHT_BOLD
    self.__texttag_minus = gtk.TextTag('minus')
    self.__texttag_minus.props.foreground = '#4169e1'
    self.__texttag_minus.props.weight = pango.WEIGHT_BOLD
    self.__texttag_hunk = gtk.TextTag('hunk')
    self.__texttag_hunk.props.foreground = '#2e8b57'
    self.__texttag_hunk.props.background = '#f0fff0'
    self.__texttag_normal = gtk.TextTag('normal')
    self.__texttag_normal.props.family = 'Monospace'
    self.__texttag_error = gtk.TextTag('error')
    self.__texttag_error.props.foreground = '#c00'
    # テキストビュー
    self.__texttagtable = gtk.TextTagTable()
    self.__texttagtable.add(self.__texttag_plus3)
    self.__texttagtable.add(self.__texttag_plus)
    self.__texttagtable.add(self.__texttag_minus3)
    self.__texttagtable.add(self.__texttag_minus)
    self.__texttagtable.add(self.__texttag_hunk)
    self.__texttagtable.add(self.__texttag_normal)
    self.__texttagtable.add(self.__texttag_error)
    self.__textbuf = gtk.TextBuffer(self.__texttagtable)
    self.__textview = gtk.TextView(self.__textbuf)
    self.__textview.props.wrap_mode = gtk.WRAP_CHAR
    self.__textview.props.editable = False
    self.__textview.modify_font(pango.FontDescription('Monospace 8'))
    self.__sw = gtk.ScrolledWindow()
    self.__sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
    self.__sw.add(self.__textview)
    # ボタン
    self.__fontbtn = gtk.FontButton('Monospace 8')
    self.__button_comp = gtk.Button('Get _diff')
    # レイアウト用コンテナ
    self.__hbox_files = gtk.HBox()
    self.__hbox_files.pack_start(self.__label_file_a, expand=False, fill=False)
    self.__hbox_files.pack_start(self.__entry_file_a)
    self.__hbox_files.pack_start(self.__btn_browse_file_a, expand=False, fill=False)
    self.__hbox_files.pack_start(self.__label_file_b, expand=False, fill=False)
    self.__hbox_files.pack_start(self.__entry_file_b)
    self.__hbox_files.pack_start(self.__btn_browse_file_b, expand=False, fill=False)
    self.__hbox_btns = gtk.HBox()
    self.__hbox_btns.pack_start(self.__fontbtn)
    self.__hbox_btns.pack_start(self.__button_comp)
    self.__vbox = gtk.VBox()
    self.__vbox.pack_start(self.__menubar, expand=False, fill=False)
    self.__vbox.pack_start(self.__hbox_files, expand=False, fill=False)
    self.__vbox.pack_start(self.__sw)
    self.__vbox.pack_start(self.__hbox_btns, expand=False, fill=False)
    # シグナル
    self.connect('delete_event', gtk.main_quit)
    self.__item_quit.connect('activate', gtk.main_quit)
    self.__item_save.connect('activate', self.__on_item_save_activate)
    self.__item_saveas.connect('activate', self.__on_item_saveas_activate)
    self.__fontbtn.connect('font-set', lambda widget: self.__textview.modify_font(pango.FontDescription(widget.props.font_name)))
    self.__button_comp.connect('clicked', self.__on_button_comp_clicked)
    # ウィンドウ
    self.add(self.__vbox)
    self.set_size_request(400, 300)
    self.props.title = '(no input) - getdifftest'
    # 出力ファイル
    self.__outfile = None
  def __on_item_save_activate(self, widget):
    """
    差分を上書き保存
    """
    if self.__outfile:
      save_difffile(self.__outfile, self.__textbuf.props.text)
    else:
      self.__item_saveas.activate()
  def __on_item_saveas_activate(self, widget):
    """
    差分を別名で保存
    """
    savedlg = gtk.FileChooserDialog(title='Save diff file', action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT))
    savedlg.props.do_overwrite_confirmation = True
    savedlg.props.local_only = True
    if self.__outfile:
      savedlg.set_filename(self.__outfile)
    res = savedlg.run()
    if res == gtk.RESPONSE_ACCEPT:
      self.__outfile = savedlg.get_filename()
    savedlg.destroy()
    if res == gtk.RESPONSE_ACCEPT:
      save_difffile(self.__outfile, self.__textbuf.props.text)
      self.props.title = '%s - getdifftest' % self.__outfile
  def __append_output(self, text, tagname):
    """
    TextBufferにテキストを追加
    """
    iter = self.__textbuf.get_end_iter()
    self.__textbuf.place_cursor(iter)
    self.__textbuf.insert_with_tags_by_name(iter, text, tagname)
  def __on_button_comp_clicked(self, widget):
    """
    比較ボタンが押されたら差分をテキストビューに表示
    """
    self.__textbuf.props.text = ''
    # 各ファイルのデータをリストに読み込む
    infile_a = self.__entry_file_a.props.text
    try:
      f_in_a = open(infile_a, 'r')
    except IOError, (errno, msg):
      self.__append_output('Error: cannot open file "%s": %s' % (infile_a, msg), 'error')
      return
    infile_b = self.__entry_file_b.props.text
    try:
      f_in_b = open(infile_b, 'r')
    except IOError, (errno, msg):
      f_in_a.close()
      self.__append_output('Error: cannot open file "%s": %s' % (infile_b, msg), 'error')
      return
    try:
      try:
        text_a = f_in_a.readlines()
      except IOError, (errno, msg):
        self.__append_output('Error: cannot read file "%s": %s' % (infile_a, msg), 'error')
        f_in_b.close()
        return
    finally:
      f_in_a.close()
    try:
      try:
        text_b = f_in_b.readlines()
      except IOError, (errno, msg):
        self.__append_output('Error: cannot read file "%s": %s' % (infile_b, msg), 'error')
        return
    finally:
      f_in_b.close()
    # 比較
    g = difflib.unified_diff(text_a,
                             text_b,
                             infile_a,
                             infile_b,
                             time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getmtime(infile_a))),
                             time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getmtime(infile_b))),
                             3,
                             '\n')
    # 結果をテキストビューに表示
    for l in g:
      if l.startswith('+++'):
        self.__append_output(l, 'plus3')
      elif l.startswith('---'):
        self.__append_output(l, 'minus3')
      elif l.startswith('+'):
        self.__append_output(l, 'plus')
      elif l.startswith('-'):
        self.__append_output(l, 'minus')
      elif l.startswith('@@'):
        self.__append_output(l, 'hunk')
      else:
        self.__append_output(l, 'normal')
    self.props.title = '*new diff* - getdifftest'
    self.__outfile = None

class GetDiffTest:
  """
  unified形式のdiffを取得してテキストビューに表示
  """
  def main(self):
    """
    アプリケーションのメイン処理
    """
    win = MainWindow()
    win.show_all()
    gtk.main()


if __name__ == '__main__':
  app = GetDiffTest()
  app.main()

(2010/4/4)フォント変更の処理を改善

メニューの「(上書き)保存」と「別名で保存」が分かれているが、編集はできないので上書きの保存ができてもあまり意味はないかもしれない。
ファイルの場所を入力する2つの欄は、密かにドラッグ・アンド・ドロップでそのファイルの場所が入力されるようになっている。

使用したバージョン: