Skip to content

TerminalEmulator:SimplePythonExample

순수 Python과 tkinter 로 만든 간단한 터미널 에뮬레이터

Features

  • tkinter ScrolledText 위젯으로 터미널 화면 구현
  • subprocess로 bash 프로세스 연결
  • 실시간 명령어 입력/출력 처리
  • cd, exit 명령어 특별 처리

Usage

  • 스크립트 실행하면 검은 바탕의 터미널 창 열림
  • 일반적인 bash 명령어 사용 가능 (ls, pwd, cat 등)
  • Enter로 명령어 실행
  • exit으로 종료

제한사항

  • 매우 기본적인 구현으로 복잡한 interactive 프로그램은 제한적
  • 색상 코드나 고급 터미널 기능 미지원

코드가 약 100줄로 최대한 간결하게 구현했습니다. 실행하면 바로 bash 터미널로 사용할 수 있습니다.

Source code

#!/usr/bin/env python3
import tkinter as tk
from tkinter import scrolledtext
import subprocess
import threading
import os

class Terminal:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("터미널 에뮬레이터")
        self.root.geometry("800x600")
        self.root.configure(bg='black')

        # 터미널 출력창
        self.output = scrolledtext.ScrolledText(
            self.root, 
            bg='black', 
            fg='white', 
            font=('Consolas', 10),
            insertbackground='white'
        )
        self.output.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        # bash 프로세스 시작
        self.process = subprocess.Popen(
            ['bash'],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            bufsize=0
        )

        # 현재 입력 라인 시작 위치
        self.prompt_start = '1.0'

        # 키 바인딩
        self.output.bind('<Key>', self.on_key)
        self.output.bind('<Return>', self.on_enter)
        self.output.focus_set()

        # 출력 읽기 스레드 시작
        self.output_thread = threading.Thread(target=self.read_output, daemon=True)
        self.output_thread.start()

        # 초기 프롬프트 표시
        self.show_prompt()

    def show_prompt(self):
        """프롬프트 표시"""
        cwd = os.getcwd()
        prompt = f"user@terminal:{os.path.basename(cwd)}$ "
        self.output.insert(tk.END, prompt)
        self.prompt_start = self.output.index(tk.END + "-1c")
        self.output.see(tk.END)

    def on_key(self, event):
        """키 입력 처리"""
        # 프롬프트 이전 영역 수정 방지
        current_pos = self.output.index(tk.INSERT)
        if self.output.compare(current_pos, '<', self.prompt_start):
            return 'break'

        # 특수 키들은 허용
        if event.keysym in ['Up', 'Down', 'Left', 'Right', 'BackSpace', 'Delete']:
            if event.keysym == 'BackSpace':
                if self.output.compare(tk.INSERT, '<=', self.prompt_start):
                    return 'break'

        return None

    def on_enter(self, event):
        """Enter 키 처리"""
        # 현재 라인의 명령어 추출
        line_start = self.output.index("insert linestart")
        line_end = self.output.index("insert lineend")
        line_content = self.output.get(line_start, line_end)

        # 프롬프트 이후의 명령어만 추출
        prompt_line = self.output.get(self.prompt_start + " linestart", self.prompt_start + " lineend")
        if "$ " in prompt_line:
            prompt_end = prompt_line.rfind("$ ") + 2
            command = line_content[prompt_end:].strip()
        else:
            command = line_content.strip()

        # 새 줄 추가
        self.output.insert(tk.END, "\n")

        # 명령어 실행
        if command:
            self.execute_command(command)
        else:
            self.show_prompt()

        return 'break'

    def execute_command(self, command):
        """명령어 실행"""
        try:
            # cd 명령어 특별 처리
            if command.startswith('cd '):
                path = command[3:].strip() or os.path.expanduser('~')
                try:
                    os.chdir(os.path.expanduser(path))
                    self.show_prompt()
                except FileNotFoundError:
                    self.output.insert(tk.END, f"bash: cd: {path}: No such file or directory\n")
                    self.show_prompt()
                return

            # exit 명령어
            if command in ['exit', 'quit']:
                self.root.quit()
                return

            # 다른 명령어들
            self.process.stdin.write(command + '\n')
            self.process.stdin.flush()

        except Exception as e:
            self.output.insert(tk.END, f"Error: {str(e)}\n")
            self.show_prompt()

    def read_output(self):
        """프로세스 출력 읽기"""
        while True:
            try:
                output = self.process.stdout.readline()
                if output:
                    self.root.after(0, lambda: self.display_output(output))
                elif self.process.poll() is not None:
                    break
            except:
                break

    def display_output(self, output):
        """출력 표시"""
        self.output.insert(tk.END, output)
        self.output.see(tk.END)

        # 명령어 완료 후 새 프롬프트 표시
        if output.strip() == "" or not output.endswith('\n'):
            self.show_prompt()

    def run(self):
        """GUI 실행"""
        try:
            self.root.mainloop()
        finally:
            if self.process.poll() is None:
                self.process.terminate()

if __name__ == "__main__":
    terminal = Terminal()
    terminal.run()

See also