editor.iniview のソースコード

"""
INIファイルやテキストファイルを閲覧するためのGUIアプリケーション。

本アプリケーションは、INIファイルや一般的なテキストファイルの内容を直感的に表示・操作するためのGUIツールです。
左ペインのツリービューでINIのキー(またはファイルの内容全体)を階層的に表示し、
右ペインのテキストエディタで選択したキーに対応する値を閲覧できます。
多行のINI値の解析、検索、コピー、ウィンドウ設定の保存、および外部コマンドの実行機能も提供します。

:doc:'iniview_usage'
"""

import os
import sys
import traceback
import re
import subprocess
import tkinter.messagebox as msgbox

import ttkbootstrap

missing = []
err = ""
for lib in ["chardet", "configparser", "tkinter", "ttkbootstrap"]:
    try:
        __import__(lib)
    except ImportError:
        missing.append(lib)
        err += "\n" + traceback.format_exc()

if missing:
    msgbox.showerror("ライブラリエラー", 
         f"以下のライブラリが不足しています:\n{', '.join(missing)}"
       + "\n\n詳細:\n{err}")
    sys.exit(1)

import chardet
import configparser
import tkinter as tk
import ttkbootstrap
from tkinter import filedialog, simpledialog
from tkinter import ttk
from ttkbootstrap import Window


SETTINGS_FILE = "iniview.ini"

[ドキュメント] def parse_multiline_ini(filepath): """ 指定されたファイルパスからINI形式のデータを解析し、辞書として返す。 ファイルのエンコーディングをchardetで自動検出します。 '.ini' 拡張子以外のファイルは、その内容全体を''all''キーの値として読み込みます。 '.ini' ファイルの場合、コメント行 ('#' で始まる行) をスキップし、多行の値をサポートします。 多行値は '\"\"\"' で囲まれた範囲として扱われます。 :param filepath: str 解析対象のファイルパス。 :returns: dict 解析された設定の辞書。キーはINIファイルのキー、値はINIファイルの値。 ファイルが存在しない場合や解析エラーの場合は空の辞書を返します。 """ settings = {} if not os.path.exists(filepath): return settings detected_encoding = 'utf-8' try: with open(filepath, 'rb') as f_raw: raw_data = f_raw.read() result = chardet.detect(raw_data) if result['encoding'] and result['confidence'] > 0.8: detected_encoding = result['encoding'] except Exception as e: print(f"Warning: Could not detect encoding for {filepath}: {e}") _, ext = os.path.splitext(filepath) if ext.lower() != '.ini': try: with open(filepath, 'r', encoding=detected_encoding, errors='ignore') as f: content = f.read() settings['all'] = content return settings except Exception as e: print(f"読み込みエラー: {e}") return {} try: with open(filepath, 'r', encoding=detected_encoding, errors='ignore') as f: content = f.read() in_multiline = False current_key = None current_value = [] for line in content.splitlines(): stripped_line = line.strip() if in_multiline: if stripped_line.endswith('\"\"\"'): current_value.append(stripped_line.rstrip('\"\"\"')) settings[current_key] = '\n'.join(current_value) in_multiline = False current_key = None current_value = [] else: current_value.append(line) elif '=' in line: if stripped_line.startswith('#'): continue key, value_part = line.split('=', 1) key = key.strip() value_part = value_part.strip() if value_part.startswith('\"\"\"'): in_multiline = True current_key = key if value_part.endswith('\"\"\"') and len(value_part) > 5: settings[key] = value_part[3:-3] in_multiline = False current_key = None else: current_value = [value_part[3:]] else: settings[key] = value_part except Exception as e: print(f"INIファイル読み込みエラー: {filepath}: {e}") return dict(sorted(settings.items()))
[ドキュメント] class IniViewer: """ INIファイルやテキストファイルをGUIで表示・操作するためのビューアアプリケーションクラス。 'ttkbootstrap' を利用したGUIで、左ペインにファイルの内容をツリービューで表示し、 右ペインで選択された項目の値をテキストエディタで表示します。 ファイルの読み込み、検索、値のコピー、ウィンドウ設定の保存・復元、外部コマンド実行などの機能を提供します。 """ def __init__(self, root, ini_path=None): """ 'IniViewer' クラスのコンストラクタ。GUI要素の初期化とイベントバインディングを行う。 メインウィンドウのタイトル設定、左右ペインの分割、ツリービューとテキストエディタのセットアップを行う。 ステータスバー、メニューバー、コンテキストメニューを作成し、イベントハンドラをバインドする。 'SETTINGS_FILE' からウィンドウ設定や前回開いたファイルパスをロードし、 必要に応じて外部コマンドをメニューに追加します。 :param root: ttkbootstrap.Window Tkinterのルートウィンドウオブジェクト。 :param ini_path: str, optional 起動時に読み込むINIファイルのパス。デフォルトはNone。 :returns: None """ self.root = root self.root.title("INI Viewer") # ペイン分割 self.pane = tk.PanedWindow(root, orient="horizontal") self.pane.pack(fill="both", expand=True) # --- 左ペイン(TreeView + Scrollbar) --- tree_frame = tk.Frame(self.pane) self.tree = ttk.Treeview(tree_frame) yscroll_tree = tk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview) xscroll_tree = tk.Scrollbar(tree_frame, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=yscroll_tree.set, xscrollcommand=xscroll_tree.set) self.tree.grid(row=0, column=0, sticky="nsew") yscroll_tree.grid(row=0, column=1, sticky="ns") xscroll_tree.grid(row=1, column=0, sticky="ew") tree_frame.rowconfigure(0, weight=1) tree_frame.columnconfigure(0, weight=1) self.pane.add(tree_frame) # --- 右ペイン(Editor + Scrollbar) --- editor_frame = tk.Frame(self.pane) self.editor = tk.Text(editor_frame, wrap="word") yscroll_editor = tk.Scrollbar(editor_frame, orient="vertical", command=self.editor.yview) xscroll_editor = tk.Scrollbar(editor_frame, orient="horizontal", command=self.editor.xview) self.editor.configure(yscrollcommand=yscroll_editor.set, xscrollcommand=xscroll_editor.set) self.editor.grid(row=0, column=0, sticky="nsew") yscroll_editor.grid(row=0, column=1, sticky="ns") xscroll_editor.grid(row=1, column=0, sticky="ew") editor_frame.rowconfigure(0, weight=1) editor_frame.columnconfigure(0, weight=1) self.pane.add(editor_frame) # Editorコンテキストメニュー self.editor_menu = tk.Menu(self.root, tearoff=0) self.editor_menu.add_command(label="コピー", command=self.copy_from_editor) self.editor.bind("<Button-3>", self.show_editor_menu) # ステータスバー self.status = tk.Label(root, text="", anchor="w") self.status.pack(fill="x", side="bottom") self.settings = {} self.item_map = {} self.last_query = None self.search_results = [] self.search_index = -1 self.last_opened_file = None self.create_menus() self.create_context_menu() # 起動時のウィンドウ設定をロード self.load_window_settings() self._add_external_commands_to_menu(self.tools_menu) # 引数でファイルを渡された場合 if ini_path: self.load_ini(ini_path) self.last_opened_file = ini_path self.root.protocol("WM_DELETE_WINDOW", self.on_close) self.tree.bind("<<TreeviewSelect>>", self.on_select) # --- メニュー生成 ---
[ドキュメント] def create_menus(self): """ アプリケーションのメニューバーを作成する。 「操作」メニュー(ファイル読み込み、検索、コピーなど)、「表示」メニュー(右端で折り返し設定)、 および「ツール」メニュー(ファイル場所を開く、ターミナルを開く、外部コマンドなど)を生成し、 それぞれのコマンドを関連付ける。 :param: None :returns: None """ menubar = tk.Menu(self.root) file_menu = tk.Menu(menubar, tearoff=0) file_menu.add_command(label="ファイル読み込み", command=self.select_file) file_menu.add_command(label="検索", command=self.search_item) file_menu.add_command(label="次を検索", command=lambda: self.navigate_search(1)) file_menu.add_command(label="前を検索", command=lambda: self.navigate_search(-1)) file_menu.add_command(label="コピー", command=self.copy_value) menubar.add_cascade(label="操作", menu=file_menu) view_menu = tk.Menu(menubar, tearoff=0) self.wrap_var = tk.BooleanVar(value=True) view_menu.add_checkbutton(label="右端で折り返す", variable=self.wrap_var, command=self.toggle_wrap) menubar.add_cascade(label="表示", menu=view_menu) tools_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="ツール", menu=tools_menu) tools_menu.add_command(label="ファイル場所を開く", command=self._open_file_location) tools_menu.add_command(label="ターミナルを開く", command=self._open_terminal) self.tools_menu = tools_menu self.root.config(menu=menubar)
[ドキュメント] def create_context_menu(self): """ ツリービューのコンテキストメニューを作成する。 ツリービュー上で右クリックした際に表示されるコンテキストメニューを生成し、 「検索」と「コピー」コマンドを追加する。 :param: None :returns: None """ self.context_menu = tk.Menu(self.root, tearoff=0) self.context_menu.add_command(label="検索", command=self.search_item) self.context_menu.add_command(label="コピー", command=self.copy_value) self.tree.bind("<Button-3>", self.show_context_menu)
def _add_external_commands_to_menu(self, menu): """ 設定ファイルから外部コマンドを読み込み、ツールメニューに追加する。 'SETTINGS_FILE' の '[paths]' セクションから 'external_cmdX_name' と 'external_cmdX_path' の形式で 定義された外部コマンドを読み込みます。 読み込んだコマンドはツールメニューに区切り線と共に表示され、クリックすると対応するコマンドが実行されます。 :param menu: tk.Menu 外部コマンドを追加するTkinterメニューオブジェクト。 :returns: None """ added_any = False i = 1 while True: cmd_name = self.paths.get(f'external_cmd{i}_name', None) cmd_path = self.paths.get(f'external_cmd{i}_path', None) if not cmd_name or not cmd_path: break if not added_any: menu.add_separator(); added_any = True menu.add_command(label=cmd_name, command=lambda cmd=cmd_path: self._run_external_command(cmd)) i += 1 if added_any: menu.add_separator() def _substitute_variables(self, command_template): """ コマンドテンプレート内のプレースホルダーを変数に置換する。 '{{file_path}}' は現在開いているファイルのパスに置換されます。 '{{VAR_NAME}}' の形式は、まずINIファイル内の同名のキーの値に置換を試み、 見つからなければ環境変数から値を検索します。 :param command_template: str 変数を含むコマンド文字列テンプレート。 :returns: str 変数が置換されたコマンド文字列。 """ if self.last_opened_file: command_template = command_template.replace("{{file_path}}", f'"{self.last_opened_file}"') def replacer(match): var_name = match.group(1) if var_name in self.settings: return self.settings[var_name] return os.environ.get(var_name, "") return re.sub(r'\{\{(.*?)\}\}', replacer, command_template) def _run_external_command(self, command_template): """ 指定された外部コマンドを実行する。 コマンドテンプレート内の変数を置換した後、'subprocess.Popen' を使用してコマンドを非同期で実行します。 コマンドの実行ディレクトリは、現在開いているファイルのディレクトリ、またはスクリプトの実行ディレクトリとなります。 実行中にエラーが発生した場合はエラーメッセージボックスを表示します。 :param command_template: str 実行する外部コマンドのテンプレート文字列。 :returns: None """ command = self._substitute_variables(command_template) try: # self.script_dir は関数内で定義されているため、ここでは直接使用 script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) if not self.last_opened_file or not os.path.exists(self.last_opened_file): cwd = script_dir else: cwd = os.path.dirname(self.last_opened_file) subprocess.Popen(command, shell=True, cwd=cwd) except Exception as e: msgbox.showerror("コマンド実行エラー", f"コマンドの実行に失敗しました:\n{command}\nエラー: {e}") def _open_file_location(self): """ 現在開いているファイルの場所をOSのファイルエクスプローラー(または同等ツール)で開く。 'last_opened_file' が設定されている場合、そのファイルのディレクトリを開きます。 設定されていない場合やファイルが存在しない場合は、スクリプトの実行ディレクトリを開きます。 OSに応じて 'explorer' (Windows), 'open' (macOS), 'xdg-open' (Linux) を使用します。 :param: None :returns: None """ script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) if not self.last_opened_file or not os.path.exists(self.last_opened_file): directory = script_dir else: directory = os.path.dirname(self.last_opened_file) directory = os.path.abspath(directory) directory = os.path.normpath(directory) if os.name == 'nt': subprocess.Popen(['explorer', directory]) elif os.uname().sysname == 'Darwin': subprocess.Popen(['open', directory]) else: subprocess.Popen(['xdg-open', directory]) def _open_terminal(self): """ 現在開いているファイルのディレクトリでターミナルを開く。 'last_opened_file' が設定されている場合、そのファイルのディレクトリをカレントディレクトリとしてターミナルを開きます。 設定されていない場合やファイルが存在しない場合は、スクリプトの実行ディレクトリで開きます。 Windowsでは 'cmd.exe' を、それ以外のOSでは 'TERMINAL' 環境変数または 'x-terminal-emulator' を使用します。 :param: None :returns: None """ script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) if not self.last_opened_file or not os.path.exists(self.last_opened_file): directory = script_dir else: directory = os.path.dirname(self.last_opened_file) if os.name == 'nt': subprocess.Popen(['cmd.exe'], cwd=directory) else: terminal_cmd = os.environ.get('TERMINAL', 'x-terminal-emulator') try: subprocess.Popen([terminal_cmd], cwd=directory) except FileNotFoundError: msgbox.showerror("エラー", f"ターミナル '{terminal_cmd}' が見つかりません。") # --- 折り返し切替 ---
[ドキュメント] def toggle_wrap(self): """ エディタのテキスト折り返し設定を切り替える。 'wrap_var' BooleanVar の値に応じて、エディタの 'wrap' オプションを '"word"' (折り返す) または '"none"' (折り返さない) に設定します。 :param: None :returns: None """ if self.wrap_var.get(): self.editor.config(wrap="word") else: self.editor.config(wrap="none")
# --- ステータスバー更新 ---
[ドキュメント] def set_status(self, msg): """ ステータスバーのテキストを更新する。 アプリケーションの下部にあるステータスバーに指定されたメッセージを表示します。 :param msg: str ステータスバーに表示するメッセージ。 :returns: None """ self.status.config(text=msg)
[ドキュメント] def show_context_menu(self, event): """ ツリービューのコンテキストメニューを表示する。 右クリックイベント発生時に、イベントの座標に基づいてコンテキストメニューを表示します。 :param event: tk.Event Tkinterのイベントオブジェクト。 :returns: None """ self.context_menu.post(event.x_root, event.y_root)
[ドキュメント] def show_editor_menu(self, event): """ エディタのコンテキストメニューを表示する。 右クリックイベント発生時に、イベントの座標に基づいてエディタのコンテキストメニューを表示します。 :param event: tk.Event Tkinterのイベントオブジェクト。 :returns: None """ self.editor_menu.post(event.x_root, event.y_root)
[ドキュメント] def select_file(self): """ ファイル選択ダイアログを開き、ユーザーが選択したファイルを読み込む。 'tk.filedialog.askopenfilename' を使用してファイル選択ダイアログを表示します。 直前に開いたファイルのディレクトリを初期ディレクトリとします。 選択されたファイルパスがあれば、'load_ini' メソッドでファイルを読み込み、 'last_opened_file' を更新し、ステータスバーにメッセージを表示します。 :param: None :returns: None """ # 直前に開いたファイルのディレクトリを既定にする script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) if self.last_opened_file and os.path.exists(self.last_opened_file): initial_dir = os.path.abspath(os.path.dirname(self.last_opened_file)) else: initial_dir = os.path.join(script_dir, "iniview") filetypes = [("Readable files", "*.ini;*.p?;*.c*;*.f*"), ("INI files", "*.ini"), ("Python/Perl files", "*.p?"), ("All files", "*.*")] filepath = filedialog.askopenfilename( filetypes = filetypes, initialdir = initial_dir) if filepath: self.load_ini(filepath) self.last_opened_file = filepath self.set_status(f"{filepath} を読み込みました")
[ドキュメント] def load_ini(self, filepath): """ 指定されたINIファイルを読み込み、ツリービューに表示する。 'parse_multiline_ini' 関数を使用してファイルを解析します。 既存のツリービューの内容をクリアし、解析結果のキーと値をツリービューに階層的に挿入します。 ウィンドウタイトルも更新します。 :param filepath: str 読み込むINIファイルのパス。 :returns: None """ self.settings = parse_multiline_ini(filepath) filename = os.path.basename(filepath) self.root.title(f"IniView.py: {filename}") self.tree.delete(*self.tree.get_children()) self.item_map.clear() for key, value in self.settings.items(): parts = key.split("\\") parent = "" for i, part in enumerate(parts): path = "\\".join(parts[:i+1]) if path not in self.item_map: item = self.tree.insert(parent, "end", text=part, open=True) self.item_map[path] = item parent = self.item_map[path] self.tree.item(self.item_map[key], values=(value,))
[ドキュメント] def search_item(self): """ ユーザーからの入力に基づいて、ツリービュー内の項目を検索する。 'simpledialog.askstring' を使用して検索文字列をユーザーに入力させます。 検索文字列がキーまたは値に含まれるすべての項目を検索し、結果を 'search_results' に保存します。 最初の検索結果に移動します。 :param: None :returns: None """ query = simpledialog.askstring("検索", "検索する文字列を入力してください") if not query: return self.last_query = query self.search_results = [ (key, item_id) for key, item_id in self.item_map.items() if query in key or query in self.settings.get(key, "") ] self.search_index = -1 self.navigate_search(1)
# --- コピー機能 ---
[ドキュメント] def copy_value(self): """ ツリービューで選択されている項目の値をクリップボードにコピーする。 選択された項目のキーに対応する値を 'settings' 辞書から取得し、 ルートウィンドウのクリップボードにコピーします。 ステータスバーにコピー完了メッセージを表示します。 :param: None :returns: None """ selected = self.tree.selection() if not selected: return for item_id in selected: for key, id_ in self.item_map.items(): if id_ == item_id: value = self.settings.get(key, "") self.root.clipboard_clear() self.root.clipboard_append(value) self.set_status("コピーしました") break
[ドキュメント] def copy_from_editor(self): """ エディタで選択されているテキスト、またはエディタ全体の内容をクリップボードにコピーする。 エディタでテキストが選択されていればその部分を、選択されていなければエディタ全体のテキストを クリップボードにコピーします。 ステータスバーにコピー完了メッセージを表示します。 :param: None :returns: None """ try: text = self.editor.get("sel.first", "sel.last") except tk.TclError: text = self.editor.get("1.0", "end-1c") self.root.clipboard_clear() self.root.clipboard_append(text) self.set_status("コピーしました")
# --- Tree選択時に右ペインに表示 ---
[ドキュメント] def on_select(self, event): """ ツリービューで項目が選択されたときに呼び出されるイベントハンドラ。 選択された項目のキーに対応する値を 'settings' 辞書から取得し、 右ペインのテキストエディタに表示します。 ステータスバーをクリアします。 :param event: tk.Event Tkinterのイベントオブジェクト。 :returns: None """ self.set_status("") # ステータスバーをクリア selected = self.tree.selection() if not selected: return for item_id in selected: for key, id_ in self.item_map.items(): if id_ == item_id: value = self.settings.get(key, "") self.editor.delete("1.0", "end") self.editor.insert("1.0", value) break
# --- 設定保存 ---
[ドキュメント] def load_window_settings(self): """ 'iniview.ini' ファイルからウィンドウの位置とサイズ、最後のファイルパス、およびパス設定をロードする。 'configparser' を使用して 'SETTINGS_FILE' を読み込みます。 以前保存されたウィンドウのジオメトリ、ペインのサッシュ位置、最後に開いたファイルパスを適用します。 外部コマンドやファイルマネージャー、ターミナルのパス設定も読み込みます。 最後に開いたファイルが存在し、パスが有効であれば、そのファイルを自動的に読み込みます。 :param: None :returns: None """ cfg = configparser.ConfigParser() if os.path.exists(SETTINGS_FILE): cfg.read(SETTINGS_FILE) if "Window" in cfg: geom = cfg["Window"].get("geometry") if geom: self.root.geometry(geom) sash = cfg["Window"].get("sash", 100) if sash: try: pos = int(sash) self.root.after(100, lambda: self.pane.sash_place(0, pos, 0)) except Exception: pass self.last_opened_file = cfg["Window"].get("last_file", None) if self.last_opened_file and os.path.exists(self.last_opened_file): self.load_ini(self.last_opened_file) self.paths = {} # script_dir は _run_external_command などで毎回計算されるため、__init__ では定義不要 self.script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) # ここで定義しておくと便利だが、既存コード変更不可のためそのまま if cfg.has_section("paths"): self.paths["file_manager_path"] = cfg["paths"].get("file_manager_path") self.paths["terminal_path"] = cfg["paths"].get("terminal_path") else: self.paths["file_manager_path"] = None self.paths["terminal_path"] = None i = 1 while True: name_key = f"external_cmd{i}_name" name_val = cfg["paths"].get(name_key, None) path_key = f"external_cmd{i}_path" path_val = cfg["paths"].get(path_key, None) if path_val is None or name_val is None: break self.paths[name_key] = name_val self.paths[path_key] = path_val i += 1
[ドキュメント] def on_close(self): """ アプリケーションが閉じられる際に呼び出されるイベントハンドラ。ウィンドウ設定を保存する。 現在のウィンドウのジオメトリ、最後に開いたファイルパス、ペインのサッシュ位置、 および外部コマンドパスを 'SETTINGS_FILE' に保存します。 その後、ルートウィンドウを破棄してアプリケーションを終了します。 :param: None :returns: None """ cfg = configparser.ConfigParser() sash = self.pane.sash_coord(0)[0] if self.pane.sash_coord(0) else 200 cfg["Window"] = { "geometry": self.root.geometry(), "last_file": getattr(self, "last_opened_file", ""), "sash": str(sash) } cfg["paths"] = { "file_manager_path": self.paths["file_manager_path"], "terminal_path": self.paths["terminal_path"], } i = 1 while True: name_key = f"external_cmd{i}_name" name_val = self.paths.get(name_key, None) path_key = f"external_cmd{i}_path" path_val = self.paths.get(path_key, None) if path_val is None or name_val is None: break cfg["paths"][name_key] = name_val cfg["paths"][path_key] = path_val i += 1 with open(SETTINGS_FILE, "w") as f: cfg.write(f) self.root.destroy()
if __name__ == "__main__": """ アプリケーションのエントリポイント。 必要なライブラリのインポートチェックを行い、不足している場合はエラーメッセージを表示して終了します。 コマンドライン引数からINIファイルのパスを取得します。 'ttkbootstrap.Window' を使ってルートウィンドウを作成し、 'IniViewer' クラスのインスタンスを生成してメインループを開始します。 :param: None :returns: None (アプリケーションの実行) """ ini_path = sys.argv[1] if len(sys.argv) > 1 else None app = Window(themename="flatly") viewer = IniViewer(app, ini_path) app.mainloop()