7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonだけで作る「MS-DOS風」フライト情報CLIクソアプリ

7
Posted at

1.はじめに

どうも、趣味でデータ分析している猫背なエンジニアです。
今回はクソアプリ企画に参加していこうと思います。

警告
これはクソアプリです。
実際の情報とは乖離します。ご注意ください。

image.png

2.「MS-DOS風」フライト情報CLIアプリ

■ 概要
みなさまは、よく飛行機に乗りますか?私はもちろん乗りません。ですが、たまにフライト情報を見たい時があります。そんなときに気軽に情報を取得できるものがあれば最高ですよね。しかもローカルで動くフライト情報表示アプリ。

■ 機能設計
機能としては以下のようになっています。
〇 それっぽいスケジュール
〇 ANSIカラーで無理やりDOS感を演出
〇 ENTERでリロードすると状況が変わる
〇 たまに勝手に遅延・欠航する
〇 うるさい広告バナー付き
〇 何もしてないのにビープ音が鳴る

■ 動作イメージ

MS-DOS Flight Info v0.13 (Crappy Edition)
┌──────────────────────────────────────────────────────────────┐
│  コマンド: [L]ist  [V]iew <flight>  [S]earch  [R]efresh  [Q]uit  │
└──────────────────────────────────────────────────────────────┘

        !!! 飛行機保険? NO THANKS!! Buy Now!! (typo free?)

FLIGHT   FROM   TO     SCH              STATUS       GATE   REMARKS
--------------------------------------------------------------------------------
ANA4890  SIN    HND    2025-12-20 12:55 Departed 6A     Check-in closes 30 min before
ND7095   SFO    CTS    2025-12-20 13:01 On Time      2A     Crew change
JAL6949  KIX    FUK    2025-12-20 13:16 Cancelled 24A    Weather hold
TOK3761  SFO    HND    2025-12-20 13:19 Arrived      33B
FUJ1193  SFO    KIX    2025-12-20 13:28 Boarding 26B    Check-in closes 30 min before
FUJ125   LAX    CTS    2025-12-20 13:55 On Time      3A     Crew change
ANA9325  SFO    CTS    2025-12-20 14:32 Delayed 26C    Check-in closes 30 min before
NRT9509  KIX    SFO    2025-12-20 14:53 Delayed 16A
SKY9547  SIN    ICN    2025-12-20 15:19 Boarding 12B    Check-in closes 30 min before
SKY6205  OKA    NGO    2025-12-20 15:34 Arrived      33B    Weather hold
NRT8216  FUK    OKA    2025-12-20 16:07 Departed 12C    Check-in closes 30 min before
JAL6727  LAX    KIX    2025-12-20 16:08 Delayed 9B     Check-in closes 30 min before
SKY4682  NGO    KIX    2025-12-20 16:14 Gate Changed 14C
FUJ5161  OKA    HKG    2025-12-20 16:40 Gate Changed 22A
ND158    NGO    NRT    2025-12-20 16:49 On Time      32C    Operated by partner
NRT8900  LAX    NGO    2025-12-20 18:11 Arrived      32B
JC7446   ICN    NGO    2025-12-20 18:12 Departed 34B
JAL2422  OKA    CTS    2025-12-20 18:17 Gate Changed 15A    Operated by partner

コマンドを入力して下さい >

■ フライト情報の生成
特殊なライブラリは一つも使っていません。
航空会社コード / 発着地 / 出発予定時刻 / ステータス(On Time / Delayed / Cancelled など) / ゲート番号は気合で生成しています。

最近のPCは素晴らしいですね。

■ 重要イベントでビープ音
遅延・欠航が発生すると…… ピッと音が鳴る設計にしています。
これで急いでいるときに何かあっても安心ですね。

■ 検索・詳細表示も一応ある
検索機能も搭載しており、自分が搭乗する便が通常通り動いているかも確認できますね。
S で検索(便名 / 出発地 / 到着地)
V JC012 で詳細表示

■ ソースコード

flight.py
import os
import sys
import time
import random
from datetime import datetime, timedelta

# --- MS-DOS風カラー(ANSI) ---
CSI = "\x1b["
RESET = CSI + "0m"
BOLD = CSI + "1m"
INVERT = CSI + "7m"
FG_GREEN = CSI + "32m"
FG_CYAN = CSI + "36m"
FG_YELLOW = CSI + "33m"
FG_RED = CSI + "31m"
FG_WHITE = CSI + "37m"

def cls():
    if os.name == "nt":
        os.system("cls")
    else:
        os.system("clear")

# --- Data model (ランダム生成) ---
AIRLINES = ["JC", "ND", "SKY", "FUJ", "TOK", "NRT", "ANA", "JAL"]
CITIES = ["NRT", "HND", "KIX", "CTS", "FUK", "OKA", "NGO", "SFO", "LAX", "HKG", "SIN", "ICN"]
GATES = [f"{i}{c}" for i in range(1,35) for c in ["A","B","C"]][:200]

STATUSES = ["On Time", "Boarding", "Delayed", "Cancelled", "Gate Changed", "Departed", "Arrived"]

def random_time_from(now, mins_min=10, mins_max=360):
    return now + timedelta(minutes=random.randint(mins_min, mins_max))

def generate_flights(n=18):
    now = datetime.now()
    flights = []
    for i in range(n):
        airline = random.choice(AIRLINES)
        num = random.randint(1,9999)
        flight_no = f"{airline}{num:03d}"
        origin = random.choice(CITIES)
        dest = random.choice([c for c in CITIES if c != origin])
        sched = random_time_from(now, mins_min=5, mins_max=480)
        # random status with bias toward On Time
        status = random.choices(STATUSES, weights=[50,15,15,2,6,8,4])[0]
        # if status is Departed/Arrived adjust times
        if status == "Departed":
            actual = sched - timedelta(minutes=random.randint(1,30))
        elif status == "Arrived":
            actual = sched + timedelta(minutes=random.randint(-30,30))
        elif status == "Delayed":
            delay = random.randint(10,120)
            actual = sched + timedelta(minutes=delay)
        else:
            actual = None
        gate = random.choice(GATES)
        flights.append({
            "flight_no": flight_no,
            "airline": airline,
            "origin": origin,
            "dest": dest,
            "sched": sched,
            "actual": actual,
            "status": status,
            "gate": gate,
            "remarks": random.choice(["", "Operated by partner", "Check-in closes 30 min before", "Crew change", "Weather hold"])
        })
    # sort by scheduled time
    flights.sort(key=lambda f: f["sched"])
    return flights

# --- UI helpers ---
def header():
    print(FG_CYAN + BOLD + "MS-DOS Flight Info v0.13 (Crappy Edition)".ljust(64) + RESET)
    print(FG_YELLOW + "" + ""*62 + "" + RESET)
    print(FG_YELLOW + "" + RESET + "  コマンド: [L]ist  [V]iew <flight>  [S]earch  [R]efresh  [Q]uit  ".ljust(62) + FG_YELLOW + "" + RESET)
    print(FG_YELLOW + "" + ""*62 + "" + RESET)

def ads_banner():
    # intentionally ugly ad with a typo
    print()
    print(FG_WHITE + INVERT + " !!! 飛行機保険? NO THANKS!! Buy Now!! (typo free?) ".center(62) + RESET)
    print()

def draw_table(flights, show_count=18):
    print(FG_GREEN + BOLD + f"{'FLIGHT':8} {'FROM':6} {'TO':6} {'SCH':16} {'STATUS':12} {'GATE':6} REMARKS" + RESET)
    print("-"*80)
    for f in flights[:show_count]:
        sched = f["sched"].strftime("%Y-%m-%d %H:%M")
        status = f["status"]
        # make delayed show red
        status_display = status
        if "Delayed" in status or "Cancelled" in status:
            status_display = FG_RED + status + RESET
        elif status in ("Boarding","Departed"):
            status_display = FG_YELLOW + status + RESET
        print(f"{f['flight_no']:8} {f['origin']:6} {f['dest']:6} {sched:16} {status_display:12} {f['gate']:6} {f['remarks']}")

def beep():
    # terminal bell
    print("\a", end="", flush=True)

def show_flight_detail(f):
    cls()
    print(FG_CYAN + BOLD + f"Flight {f['flight_no']} — Detailed Info" + RESET)
    print("-"*50)
    print(f"Airline : {f['airline']}")
    print(f"From    : {f['origin']}")
    print(f"To      : {f['dest']}")
    print(f"Sched   : {f['sched'].strftime('%Y-%m-%d %H:%M')}")
    if f["actual"]:
        print(f"Actual  : {f['actual'].strftime('%Y-%m-%d %H:%M')}")
    else:
        print("Actual  : --")
    print(f"Status  : {f['status']}")
    print(f"Gate    : {f['gate']}")
    print(f"Remarks : {f['remarks']}")
    print()
    print(FG_YELLOW + "Press Enter to go back..." + RESET)
    input()

def search_flights(flights, q):
    q = q.strip().upper()
    res = [f for f in flights if q in f["flight_no"].upper() or q == f["origin"].upper() or q == f["dest"].upper()]
    return res

# --- Main loop ---
def main():
    flights = generate_flights(24)
    # intentionally broken greeting
    cls()
    print(FG_WHITE + INVERT + " WELCOME! THIS APP IS BUGGY & PROUD. ".center(62) + RESET)
    print()
    time.sleep(0.6)
    while True:
        cls()
        header()
        ads_banner()
        draw_table(flights, show_count=18)
        print()
        cmd = input(FG_CYAN + "コマンドを入力して下さい > " + RESET).strip()
        if cmd == "":
            # treat Enter as refresh (lazy)
            # small chance to randomly mutate a status (simulate 'live' chaotic updates)
            if random.random() < 0.35:
                idx = random.randrange(len(flights))
                old = flights[idx]["status"]
                flights[idx]["status"] = random.choice(STATUSES)
                # sometimes change gate
                if random.random() < 0.3:
                    flights[idx]["gate"] = random.choice(GATES)
                # if change to delayed, bump actual
                if flights[idx]["status"] == "Delayed":
                    if flights[idx]["actual"]:
                        flights[idx]["actual"] += timedelta(minutes=random.randint(10,60))
                    else:
                        flights[idx]["actual"] = flights[idx]["sched"] + timedelta(minutes=random.randint(10,90))
                # beep if important
                if "Cancelled" in flights[idx]["status"] or "Delayed" in flights[idx]["status"]:
                    beep()
            continue

        c = cmd.lower().split()
        if c[0] in ("q","quit","exit"):
            cls()
            print(FG_RED + "Bye! このクソアプリを使ってくれてありがとう (not)".center(60) + RESET)
            break
        elif c[0] in ("l","list"):
            # just redraw (loop already shows list)
            continue
        elif c[0] in ("r","refresh"):
            # manual refresh = mutate some flights and re-generate tiny chance
            for _ in range(random.randint(0,3)):
                idx = random.randrange(len(flights))
                flights[idx]["status"] = random.choice(STATUSES)
            print(FG_YELLOW + "Refreshed (but maybe not)..." + RESET)
            time.sleep(0.6)
            continue
        elif c[0] in ("v","view"):
            if len(c) < 2:
                print(FG_RED + "使い方: V <FLIGHT_NO> 例: V JC012" + RESET)
                time.sleep(1.2)
                continue
            q = c[1].upper()
            found = [f for f in flights if f["flight_no"].upper() == q]
            if not found:
                print(FG_RED + "フライトが見つかりません (当然かも)..." + RESET)
                time.sleep(1.2)
                continue
            show_flight_detail(found[0])
            continue
        elif c[0] in ("s","search"):
            if len(c) < 2:
                q = input("検索ワード (便名 or FROM or TO) > ").strip()
            else:
                q = " ".join(c[1:])
            res = search_flights(flights, q)
            if not res:
                print(FG_RED + "該当なし。Your search sucks." + RESET)
                time.sleep(1.2)
                continue
            # show small result table
            cls()
            print(FG_CYAN + f"Search results for '{q}':" + RESET)
            draw_table(res, show_count=50)
            print()
            print(FG_YELLOW + "Enter flight no to view details, or press Enter to go back." + RESET)
            choice = input("> ").strip().upper()
            if choice:
                fnd = [f for f in res if f["flight_no"].upper() == choice]
                if fnd:
                    show_flight_detail(fnd[0])
                else:
                    print(FG_RED + "Not found in search results." + RESET)
                    time.sleep(1.0)
            continue
        else:
            # unknown command: show snarky message
            print(FG_RED + "??? コマンドが無効です。あなたは何をしているの?" + RESET)
            time.sleep(1.0)
            continue

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n" + FG_RED + "Interrupted. クソアプリを強制終了しました。" + RESET)
        sys.exit(0)

■ 表示結果
image.png

3. おわりに

今回は前回のコウメ.pyに引き続き、実用性はない満足度は高いアプリを作りました。
今後はここから得た技術要素をもとに何か得られたらええなと思います(笑)

7
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?