Linux

【Python】Tera Termの様なターミナルアプリを自作

Tera Term(テラターム)とはWindows用ターミナルエミュレータです。 SSH・telnet・シリアルの各通信プロトコルに対応していてネットワーク上の別のコンピュータを操作することができます。 一連のコマンド入力動作をマクロ化して自動化できる点が特徴です。 オープンソースソフトウェアとして公開されています。

Tera Termは素晴らしいソフトで私自身、業務で使用させていただいています。
今回の記事は「自分の普段使っているツールを自分の手で作りたい」という思いで書いております。

https://teratermproject.github.io

Tera TermはWindows用のターミナルエミュレータなのでMACやLinuxでは動作しません。
今回は、クロスプラットフォームで動作できる様にPythonを用いてTera Termの様なターミナルエミュレータを作成したいと思います。

自作するにあたり

最近では、多機能なターミナルアプリケーションがさまざまな場面で利用されています。これらのアプリケーションは、ネットワーク機器の管理、システム監視、スクリプトの実行など、様々な用途に対応できる機能を備えています。

自作するアプリケーションの概要

今回作成するアプリケーションは、以下の機能を備えたターミナルインターフェースを実装したいと思います。

  • マルチタブインターフェース: 複数の接続やコマンドを同時に管理できるタブ機能。
  • 接続サポート: シリアル、SSH、Telnet接続をサポート。
  • スクリプトエディタ: カスタムスクリプトを実行するためのエディタ機能。
  • マクロ管理: コマンドの保存と実行を行うマクロ管理機能。
  • 接続履歴管理: 過去の接続情報を管理する機能。
  • ファイルのアップロードとダウンロード: ファイル転送機能。
  • テーマカラーの動的変更: アプリケーションのテーマカラーを変更する機能。

これらの機能を実現するために、アプリケーションはtkinterライブラリを使用してGUIを構築し、その他のライブラリ(paramikotelnetlib3serialなど)を使用して異なる接続プロトコルをサポートしています。

事前にTerminalで次のコマンドを実行して必須ライブラリーをインストールして下さい。

pip install pyserial paramiko telnetlib3 tkinter

また、私の環境は、Python 3.12.0 です。
Python3が入っている環境であれば問題なく動作します。

実装

早速実装しながら解説します。

"""
TerminalApp - Python ターミナルアプリケーション

このアプリケーションは、複数の接続タイプ(シリアル、SSH、Telnet)をサポートするターミナルインターフェースを提供します。
ユーザーは異なる接続やスクリプトのために複数のタブを開くことができ、ログを保存したり、テーマを変更したりできます。
カスタムスクリプトを実行するためのスクリプトエディタと、コマンドの保存と実行のためのマクロ管理機能を備えています。
接続履歴の管理、ファイルのアップロードとダウンロード、テーマカラーの動的変更も可能です。

機能:
- 異なる接続とコマンドのためのマルチタブインターフェース
- シリアル、SSH、Telnet接続サポート
- カスタムスクリプトを実行するためのスクリプトエディタ
- コマンドの保存と実行のためのマクロ管理
- 接続履歴の管理
- ファイルのアップロードとダウンロード機能
- 動的なテーマカラーの変更
"""

import tkinter as tk
from tkinter import ttk, scrolledtext, filedialog, simpledialog, messagebox, colorchooser, PhotoImage
import paramiko
import telnetlib3
import serial
import subprocess
import json
import os
import asyncio
import traceback


class MacroManager:
    def __init__(self):
        self.macros = {}

    def add_macro(self, name, command):
        """Add a new macro with the given name and command."""
        self.macros[name] = command

    def execute_macro(self, name, text_area):
        """Execute the macro with the given name and insert the command into the text area."""
        if name in self.macros:
            command = self.macros[name]
            text_area.insert(tk.END, f"Executing Macro: {command}\n")
        else:
            text_area.insert(tk.END, "Macro not found\n")

class ScriptEditor(tk.Toplevel):
    def __init__(self, parent):
        """Initialize the Script Editor window with text area and run button."""
        super().__init__(parent)
        self.title("Script Editor")
        self.geometry("500x400")

        self.text_area = scrolledtext.ScrolledText(self, wrap=tk.WORD, width=60, height=20)
        self.text_area.pack(padx=10, pady=10)

        self.run_button = tk.Button(self, text="Run Script", command=self.run_script)
        self.run_button.pack(padx=10, pady=10)

    def load_script(self, path):
        """Load the script from the specified file path into the text area."""
        with open(path, 'r') as file:
            script_content = file.read()
            self.text_area.insert(tk.END, script_content)

    def run_script(self):
        """Execute the script code from the text area."""
        script_code = self.text_area.get("1.0", tk.END)
        try:
            exec(script_code, {'app': self.master})
            self.master.text_area.insert(tk.END, "Script executed successfully\n")
        except Exception as e:
            self.master.text_area.insert(tk.END, f"Error executing script: {e}\n")
            traceback.print_exc()

class TerminalApp:
    def __init__(self, root):
        """Initialize the main application window and set up the UI elements."""
        self.root = root
        self.root.title("My Terminal APP")
        
        # Initialize connection variables
        self.ssh_conn = None
        self.telnet_reader = None
        self.telnet_writer = None
        self.serial_conn = None
        
        # Initialize the macro manager
        self.macro_manager = MacroManager()

        # Manage connection history
        self.config_file = "connection_history.json"
        self.load_connection_history()

        # Set up the tab widget
        self.notebook = ttk.Notebook(root)
        self.notebook.pack(expand=True, fill='both')

        # Set up the status bar
        self.status_bar = tk.Label(root, text="Not Connected", bd=1, relief=tk.SUNKEN, anchor=tk.W)
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
        
        # Set up the app_icon
        self.set_icon()

        # Create the first tab
        self.create_new_tab()

        # Set up the menu
        menu = tk.Menu(root)
        root.config(menu=menu)
        
        connection_menu = tk.Menu(menu)
        menu.add_cascade(label="Connections", menu=connection_menu)
        connection_menu.add_command(label="New Tab", command=self.create_new_tab)
        connection_menu.add_command(label="Close Tab", command=self.close_current_tab)
        connection_menu.add_command(label="Connect Serial", command=self.connect_serial)
        connection_menu.add_command(label="Connect SSH", command=self.connect_ssh)
        connection_menu.add_command(label="Connect Telnet", command=self.connect_telnet)
        connection_menu.add_command(label="Disconnect", command=self.disconnect)
        
        history_menu = tk.Menu(connection_menu)
        connection_menu.add_cascade(label="Connection History", menu=history_menu)
        for idx, history in enumerate(self.connection_history):
            history_menu.add_command(label=f"{history['type']} - {history.get('hostname', history.get('port'))}",
                                     command=lambda idx=idx: self.connect_from_history(idx))
        
        menu.add_command(label="Save Log", command=self.save_log)
        menu.add_command(label="Run Script", command=self.run_script)
        menu.add_command(label="Open Script Editor", command=self.open_script_editor)
        
        macro_menu = tk.Menu(menu)
        menu.add_cascade(label="Macros", menu=macro_menu)
        macro_menu.add_command(label="Add Macro", command=self.add_macro)
        macro_menu.add_command(label="Execute Macro", command=self.execute_macro)
        
        menu.add_command(label="Upload File", command=self.upload_file)
        menu.add_command(label="Download File", command=self.download_file)
        menu.add_command(label="Change Theme", command=self.change_theme)
        
        
    def set_icon(self):
        """Set up the app_icon"""
        icon_path = "icon.png"
        try:
            self.root.iconphoto(False, PhotoImage(file=icon_path))
        except Exception as e:
            print(f"Failed to load icon: {e}")

    def load_connection_history(self):
        """Load connection history from the configuration file."""
        if os.path.exists(self.config_file):
            with open(self.config_file, 'r') as file:
                self.connection_history = json.load(file)
        else:
            self.connection_history = []

    def save_connection_history(self):
        """Save connection history to the configuration file."""
        with open(self.config_file, 'w') as file:
            json.dump(self.connection_history, file)

    def create_new_tab(self):
        """Create a new tab with a text area and entry widget for command input."""
        new_frame = ttk.Frame(self.notebook)
        self.notebook.add(new_frame, text=f"Tab {len(self.notebook.tabs()) + 1}")
        
        text_area = tk.Text(new_frame, wrap=tk.WORD, width=80, height=20)
        text_area.pack(expand=True, fill='both')
        
        entry = tk.Entry(new_frame, width=80)
        entry.pack()
        
        entry.focus_set()
        
        entry.bind('<Return>', lambda event, ta=text_area: self.send_command(event, ta))
        # print("Entry widget created and bound")  # Debug

    def close_current_tab(self):
        """Close the currently selected tab."""
        current_tab = self.notebook.select()
        if current_tab:
            self.notebook.forget(current_tab)

    def send_command(self, event, text_area):
        """Send the command from the entry widget to the appropriate connection and display the output."""
        command = event.widget.get()
        print(f"send command: {command}") # Debug
        output = ""
        try:
            if self.ssh_conn:
                stdin, stdout, stderr = self.ssh_conn.exec_command(command)
                stdout.channel.recv_exit_status()  # Wait for command to complete
                output = stdout.read().decode() + stderr.read().decode()
            elif self.telnet_writer:
                self.telnet_writer.write(command + '\n')
                output = asyncio.run(self.read_telnet_response())
            elif self.serial_conn:
                self.serial_conn.write((command + '\n').encode())
                output = self.serial_conn.read(self.serial_conn.in_waiting).decode()
            else:
                output = subprocess.run(command, shell=True, capture_output=True, text=True).stdout
        except Exception as e:
            output = f"Error executing command: {e}\n"
            traceback.print_exc()

        text_area.insert(tk.END, f"{command}\n{output}\n")
        event.widget.delete(0, tk.END)

    async def read_telnet_response(self):
        """Read the response from a Telnet connection asynchronously."""
        try:
            response = await asyncio.wait_for(self.telnet_reader.read(1024), timeout=5)
            return response
        except asyncio.TimeoutError:
            return "Telnet response timed out."
        except Exception as e:
            traceback.print_exc()
            return f"Error reading Telnet response: {e}"

    def update_status(self, status):
        """Update the status bar with the given status text."""
        self.status_bar.config(text=status)

    def connect_serial(self):
        """Establish a serial connection using the specified port and baudrate."""
        port = simpledialog.askstring("Serial Connection", "Enter Serial Port (e.g., COM3 or /dev/tty.usbserial):")
        baudrate = simpledialog.askinteger("Serial Connection", "Enter Baudrate:", initialvalue=9600)
        if port and baudrate:
            try:
                self.serial_conn = serial.Serial(port=port, baudrate=baudrate, timeout=1)
                self.update_status(f"Connected to Serial Port {port}")
                connection_details = {"type": "serial", "port": port, "baudrate": baudrate}
                self.connection_history.append(connection_details)
                self.save_connection_history()
            except Exception as e:
                messagebox.showerror("Connection Error", str(e))
                traceback.print_exc()

    def connect_ssh(self):
        """Establish an SSH connection using the specified hostname, port, username, and keyfile/password."""
        hostname = simpledialog.askstring("SSH Connection", "Enter Hostname:")
        port = simpledialog.askinteger("SSH Connection", "Enter Port (default 22):", initialvalue=22)
        username = simpledialog.askstring("SSH Connection", "Enter Username:")
        keyfile = filedialog.askopenfilename(title="Select SSH Private Key", filetypes=[("Private Key Files", "*.pem"), ("All Files", "*.*")])
        password = simpledialog.askstring("SSH Connection", "Enter Password (leave blank if using keyfile):", show='*')
        
        print('SSH Start:') # Debug
        if hostname and username:
            self.ssh_conn = paramiko.SSHClient()
            self.ssh_conn.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            try:
                if keyfile:
                    self.ssh_conn.connect(hostname, port=port, username=username, key_filename=keyfile)
                else:
                    self.ssh_conn.connect(hostname, port=port, username=username, password=password)
                self.update_status(f"Connected to SSH {hostname}:{port}")
                connection_details = {"type": "ssh", "hostname": hostname, "username": username, "port": port, "keyfile": keyfile}
                self.connection_history.append(connection_details)
                self.save_connection_history()
                print('Connection OK.') # Debug
            except Exception as e:
                messagebox.showerror("Connection Error", str(e))
                traceback.print_exc()
                print(f'Connection Error: {e}') # Debug

    def connect_telnet(self):
        """Establish a Telnet connection using the specified hostname and port."""
        hostname = simpledialog.askstring("Telnet Connection", "Enter Hostname:")
        port = simpledialog.askinteger("Telnet Connection", "Enter Port (default 23):", initialvalue=23)
        
        if hostname:
            asyncio.run(self.telnet_connect(hostname, port))

    async def telnet_connect(self, hostname, port):
        try:
            self.telnet_reader, self.telnet_writer = await telnetlib3.open_connection(hostname, port)
            self.update_status(f"Connected to Telnet {hostname}:{port}")
            connection_details = {"type": "telnet", "hostname": hostname, "port": port}
            self.connection_history.append(connection_details)
            self.save_connection_history()
        except Exception as e:
            messagebox.showerror("Connection Error", str(e))
            traceback.print_exc()

    def connect_from_history(self, idx):
        """Connect to a previously saved connection from the history."""
        history = self.connection_history[idx]
        connection_type = history.get('type')
        if connection_type == 'serial':
            self.connect_serial_from_history(history)
        elif connection_type == 'ssh':
            self.connect_ssh_from_history(history)
        elif connection_type == 'telnet':
            asyncio.run(self.telnet_connect(history['hostname'], history['port']))

    def connect_serial_from_history(self, history):
        try:
            self.serial_conn = serial.Serial(port=history['port'], baudrate=history['baudrate'], timeout=1)
            self.update_status(f"Connected to Serial Port {history['port']}")
        except Exception as e:
            messagebox.showerror("Connection Error", str(e))
            traceback.print_exc()

    def connect_ssh_from_history(self, history):
        try:
            self.ssh_conn = paramiko.SSHClient()
            self.ssh_conn.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            self.ssh_conn.connect(history['hostname'], port=history['port'], username=history['username'], key_filename=history['keyfile'])
            self.update_status(f"Connected to SSH {history['hostname']}:{history['port']}")
        except Exception as e:
            messagebox.showerror("Connection Error", str(e))
            traceback.print_exc()

    def disconnect(self):
        """Disconnect the current connection and clean up resources."""
        try:
            if self.ssh_conn:
                self.ssh_conn.close()
                self.ssh_conn = None
                self.update_status("SSH Disconnected")
            if self.telnet_writer:
                self.telnet_writer.close()
                self.telnet_writer = None
                self.telnet_reader = None
                self.update_status("Telnet Disconnected")
            if self.serial_conn:
                self.serial_conn.close()
                self.serial_conn = None
                self.update_status("Serial Disconnected")
        except Exception as e:
            messagebox.showerror("Disconnection Error", str(e))
            traceback.print_exc()

    def save_log(self):
        log_content = self.notebook.nametowidget(self.notebook.select()).winfo_children()[0].get("1.0", tk.END)
        save_path = filedialog.asksaveasfilename(defaultextension=".log", filetypes=[("Log Files", "*.log"), ("All Files", "*.*")])
        if save_path:
            with open(save_path, 'w') as log_file:
                log_file.write(log_content)

    def run_script(self):
        script_path = filedialog.askopenfilename(filetypes=[("Python Scripts", "*.py"), ("All Files", "*.*")])
        if script_path:
            try:
                with open(script_path, 'r') as script_file:
                    script_code = script_file.read()
                    exec(script_code, {'app': self})
            except Exception as e:
                messagebox.showerror("Script Error", str(e))
                traceback.print_exc()

    def open_script_editor(self):
        script_editor = ScriptEditor(self)
        script_editor.grab_set()

    def add_macro(self):
        name = simpledialog.askstring("Add Macro", "Enter Macro Name:")
        command = simpledialog.askstring("Add Macro", "Enter Command:")
        if name and command:
            self.macro_manager.add_macro(name, command)

    def execute_macro(self):
        name = simpledialog.askstring("Execute Macro", "Enter Macro Name:")
        if name:
            self.macro_manager.execute_macro(name, self.notebook.nametowidget(self.notebook.select()).winfo_children()[0])

    def upload_file(self):
        """Prompt the user to select a file for upload and handle the file transfer."""
        if self.ssh_conn:
            local_file = filedialog.askopenfilename(title="Select File to Upload")
            remote_path = simpledialog.askstring("Remote Path", "Enter Remote Path to Upload:")
            if local_file and remote_path:
                try:
                    sftp = self.ssh_conn.open_sftp()
                    sftp.put(local_file, remote_path)
                    sftp.close()
                    self.notebook.nametowidget(self.notebook.select()).winfo_children()[0].insert(tk.END, f"File uploaded: {local_file} to {remote_path}\n")
                except Exception as e:
                    messagebox.showerror("Upload Error", str(e))
                    traceback.print_exc()
        else:
            messagebox.showwarning("Upload Warning", "SSH connection is required for uploading files.")

    def download_file(self):
        """Prompt the user to select a file for download and handle the file transfer."""
        if self.ssh_conn:
            remote_file = simpledialog.askstring("Remote File", "Enter Remote File Path:")
            local_path = filedialog.asksaveasfilename(title="Save File As")
            if remote_file and local_path:
                try:
                    sftp = self.ssh_conn.open_sftp()
                    sftp.get(remote_file, local_path)
                    sftp.close()
                    self.notebook.nametowidget(self.notebook.select()).winfo_children()[0].insert(tk.END, f"File downloaded: {remote_file} to {local_path}\n")
                except Exception as e:
                    messagebox.showerror("Download Error", str(e))
                    traceback.print_exc()
        else:
            messagebox.showwarning("Download Warning", "SSH connection is required for downloading files.")

    def change_theme(self):
        """Open a color chooser dialog to change the application theme."""
        color = colorchooser.askcolor()
        if color:
            self.notebook.nametowidget(self.notebook.select()).winfo_children()[0].config(bg=color)


if __name__ == "__main__":
    root = tk.Tk()
    app = TerminalApp(root)
    root.mainloop()

「teraterm_clone.py」と名前をつけて保存し、保存したファイルと同ディレクトリ内に「icon.png」を配置して下さい。

実装内容の解説と実行

シリアル通信のサポート: /dev/tty.usbserialポートに接続し、コマンドを送信する機能。SSH接続のサポート: Paramikoを使用してSSHサーバーに接続し、コマンドを実行する機能。Telnet接続のサポート: Telnetを使用してリモートサーバーに接続し、コマンドを実行する機能。

python teraterm_clone.py

上記コマンドをターミナルで入力し、コードを実行すると、シンプルなターミナルインターフェースが表示されます。メニューからシリアル、SSH、Telnetの接続を選択し、入力ボックスにコマンドを入力してエンターキーを押すと、コマンドが送信されて結果が表示されます。

本家のTera Termと比べると簡易的ですが、最低限の機能を実装してあります。

詳細な解説

少し長いコードなので、ポイントをもう少し解説します。

インポート部分

import tkinter as tk
from tkinter import ttk, scrolledtext, filedialog, simpledialog, messagebox, colorchooser, PhotoImage
import paramiko
import telnetlib3
import serial
import subprocess
import json
import os
import asyncio
import traceback
  • tkinter とそのサブモジュール (ttk, scrolledtext, filedialog, simpledialog, messagebox, colorchooser, PhotoImage) は、GUIアプリケーションの作成に使用されます。
  • paramiko は SSH 接続を管理するためのライブラリです。
  • telnetlib3 は Telnet 接続のためのライブラリです。
  • serial はシリアルポートとの通信に使用されます。
  • subprocess は外部コマンドの実行に使用されます。
  • jsonos は設定の読み書きやファイル管理に使用されます。
  • asyncio は非同期操作のために使用されます。
  • traceback はエラーのスタックトレースを表示するためのライブラリです。

MacroManager クラス

class MacroManager:
    def __init__(self):
        self.macros = {}

    def add_macro(self, name, command):
        self.macros[name] = command

    def execute_macro(self, name, text_area):
        if name in self.macros:
            command = self.macros[name]
            text_area.insert(tk.END, f"Executing Macro: {command}\n")
        else:
            text_area.insert(tk.END, "Macro not found\n")
  • マクロを管理するためのクラスです。
  • add_macro メソッドでマクロを追加し、execute_macro メソッドで指定したマクロを実行します。

本家、Tera Termもマクロ機能が非常に便利ですが、今回は簡易的なものを実装してあります。

ScriptEditor クラス

class ScriptEditor(tk.Toplevel):
    def __init__(self, parent):
        super().__init__(parent)
        self.title("Script Editor")
        self.geometry("500x400")

        self.text_area = scrolledtext.ScrolledText(self, wrap=tk.WORD, width=60, height=20)
        self.text_area.pack(padx=10, pady=10)

        self.run_button = tk.Button(self, text="Run Script", command=self.run_script)
        self.run_button.pack(padx=10, pady=10)

    def load_script(self, path):
        with open(path, 'r') as file:
            script_content = file.read()
            self.text_area.insert(tk.END, script_content)

    def run_script(self):
        script_code = self.text_area.get("1.0", tk.END)
        try:
            exec(script_code, {'app': self.master})
            self.master.text_area.insert(tk.END, "Script executed successfully\n")
        except Exception as e:
            self.master.text_area.insert(tk.END, f"Error executing script: {e}\n")
            traceback.print_exc()
  • ScriptEditor は新しいウィンドウを作成し、スクリプトの編集と実行を可能にします。
  • load_script メソッドでファイルからスクリプトを読み込み、run_script メソッドでスクリプトを実行します。

TerminalApp クラス

class TerminalApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Python Terminal App")
        # その他の初期化コード

ユーザーインターフェイスを生成している部分です。
主に次の様に実装してあります。

  • このクラスはアプリケーションのメインウィンドウを管理し、接続、タブ、メニューなどの機能を提供します。
  • self.ssh_conn, self.telnet_reader, self.telnet_writer, self.serial_conn は、それぞれの接続タイプの接続オブジェクトです。
  • self.macro_manager はマクロを管理する MacroManager のインスタンスです。
  • self.config_file は接続履歴の保存先ファイル名です。
メソッド
  • load_connection_history: 設定ファイルから接続履歴を読み込みます。
  • save_connection_history: 接続履歴を設定ファイルに保存します。
  • create_new_tab: 新しいタブを作成し、そのタブに Text ウィジェットと Entry ウィジェットを追加します。
  • close_current_tab: 現在のタブを閉じます。
  • send_command: 入力されたコマンドを実行し、結果を表示します。
  • read_telnet_response: Telnet の非同期応答を読み取ります。
  • update_status: ステータスバーのテキストを更新します。
  • connect_serial, connect_ssh, connect_telnet: 各種接続を管理します。
  • connect_from_history: 履歴から接続を復元します。
  • disconnect: 現在の接続を切断します。
  • save_log: 現在のタブの内容をログファイルとして保存します。
  • run_script: スクリプトファイルを選択し、実行します。
  • open_script_editor: スクリプトエディタウィンドウを開きます。
  • add_macro, execute_macro: マクロの追加と実行を行います。
  • upload_file, download_file: ファイルのアップロードとダウンロードを管理します。
  • change_theme: テーマの色を変更します。

実行部分

if __name__ == "__main__":
    root = tk.Tk()
    app = TerminalApp(root)
    root.mainloop()
  • アプリケーションを起動するためのエントリーポイントです。tk.Tk() でメインウィンドウを作成し、TerminalApp のインスタンスを生成して、mainloop() でアプリケーションのイベントループを開始します。

ターミナル接続、スクリプト実行、マクロ管理などの機能を持つ GUI アプリケーションの最低限必要な機能を実装しました。

最低限、自分が欲しかった機能として、「接続履歴の管理」機能は実装してあります。

アプリケーションの拡張アイディア

このアプリケーションは基礎的な内容を実装したに過ぎず、皆さんの業務に合わせて、拡張していく事で実用に耐え得る便利なツールになります。

私の方で、パッと思いつく限りで次の様な拡張を行うと良いかと思います。

履歴機能の強化

接続履歴だけでなく、実行したコマンドの履歴やエラーメッセージの履歴を管理し、簡単に参照できるようにします。

  • コマンド履歴管理: 実行したコマンドを履歴として保存し、履歴からコマンドを再実行できるようにします。
  • エラーログ: エラーメッセージを履歴として保存し、エラーの詳細を後で確認できるようにします。
class HistoryManager:
    def __init__(self):
        self.command_history = []
        self.error_history = []

    def add_command(self, command):
        """Add a command to the command history."""
        self.command_history.append(command)

    def add_error(self, error_message):
        """Add an error message to the error history."""
        self.error_history.append(error_message)

    def get_command_history(self):
        """Get the command history."""
        return self.command_history

    def get_error_history(self):
        """Get the error history."""
        return self.error_history

    def save_histories(self):
        """Save command and error histories to files."""
        with open('command_history.json', 'w') as file:
            json.dump(self.command_history, file)

        with open('error_history.json', 'w') as file:
            json.dump(self.error_history, file)

    def load_histories(self):
        """Load command and error histories from files."""
        if os.path.exists('command_history.json'):
            with open('command_history.json', 'r') as file:
                self.command_history = json.load(file)

        if os.path.exists('error_history.json'):
            with open('error_history.json', 'r') as file:
                self.error_history = json.load(file)

リモートファイルブラウザ

接続したリモートサーバー上のファイルシステムをブラウズできるファイルブラウザを追加します。

  • ファイルリスト表示: リモートサーバーのディレクトリの内容を表示する。
  • ファイル操作: ファイルのアップロード、ダウンロード、削除、名前変更などの操作を提供します。
class RemoteFileBrowser(tk.Toplevel):
    def __init__(self, parent, connection):
        """Initialize the Remote File Browser window."""
        super().__init__(parent)
        self.title("Remote File Browser")
        self.geometry("600x400")

        self.connection = connection
        self.file_list = tk.Listbox(self, selectmode=tk.SINGLE)
        self.file_list.pack(expand=True, fill=tk.BOTH)

        self.refresh_button = tk.Button(self, text="Refresh", command=self.refresh)
        self.refresh_button.pack(side=tk.BOTTOM, fill=tk.X)

        self.refresh()

    def refresh(self):
        """Refresh the file list from the remote server."""
        self.file_list.delete(0, tk.END)
        # Example implementation for SSH
        stdin, stdout, stderr = self.connection.exec_command('ls -l')
        files = stdout.read().decode().splitlines()
        for file in files:
            self.file_list.insert(tk.END, file)

通知機能

接続やファイル転送の進行状況を通知する機能を追加します。

  • 通知表示: タスクの進行状況をユーザーに通知するためのポップアップやステータスメッセージを表示します。
class NotificationManager:
    def __init__(self, parent):
        self.parent = parent

    def show_notification(self, title, message):
        """Show a notification popup."""
        tk.messagebox.showinfo(title, message, parent=self.parent)

カスタムコマンドのサポート

ユーザーが定義したカスタムコマンドを簡単に登録し、実行できるようにします。

  • カスタムコマンドの登録: ユーザーがカスタムコマンドを追加できるUIを提供します。
  • コマンドの実行: 登録したカスタムコマンドを選択して実行します。
class CustomCommandManager:
    def __init__(self):
        self.commands = {}

    def add_command(self, name, command):
        """Add a custom command."""
        self.commands[name] = command

    def execute_command(self, name, text_area):
        """Execute a custom command."""
        if name in self.commands:
            command = self.commands[name]
            text_area.insert(tk.END, f"Executing Custom Command: {command}\n")
            # Execute the command here

複数ユーザー対応

複数のユーザーが同時に異なるセッションでアプリケーションを使用できるようにします。

  • ユーザー管理: 各ユーザーのセッションを管理し、ログイン機能を提供します。
  • セッション切り替え: ユーザーごとの設定や接続履歴を保存し、セッションを切り替えられるようにします。
class UserManager:
    def __init__(self):
        self.users = {}

    def add_user(self, username, password):
        """Add a new user."""
        self.users[username] = password

    def authenticate(self, username, password):
        """Authenticate a user."""
        return self.users.get(username) == password

    def save_users(self):
        """Save user data to a file."""
        with open('users.json', 'w') as file:
            json.dump(self.users, file)

    def load_users(self):
        """Load user data from a file."""
        if os.path.exists('users.json'):
            with open('users.json', 'r') as file:
                self.users = json.load(file)

これらの機能を追加することで、さらに使いやすく、強力なツールになります。ここで記載した拡張アイディアは私が思いついたものを簡単な実装例で紹介させていただきました。
皆さんも、思いつくままにコードを拡張してみて下さい。

まとめ

今回はTera Termを参考にしながら、マルチタブインターフェース、スクリプトエディタ、マクロ管理機能などを持ったターミナルアプリケーションを作成しました。

皆さんも、普段開発や趣味で使用しているアプリケーションやサービスをご自身の使いやすい形で実装してみると勉強にもなりますし是非、取り組んでみて下さい。