Skip to main content

atmos/apps/terminal/
shell.rs

1//! # ターミナル用対話型シェル・コマンドプロセッサ
2//!
3//! このモジュールは、ターミナルに入力されたコマンドライン文字列の解釈と、
4//! `ls` や `cd` などのエイリアス展開、または `clear` や `history` などの
5//! ローカルコマンドのインターセプト処理を提供します。
6
7use crate::apps::terminal::Terminal;
8use crate::kernel::keyboard::Keyboard;
9use alloc::string::String;
10use alloc::vec::Vec;
11
12/// 入力されたコマンドライン文字列のエイリアスを展開し、Aura プログラミング言語の
13/// スクリプト実行コードへ変換します。
14///
15/// 例えば、`ls /user` は `fs.ls("/user")` に、`pwd` は `fs.pwd()` に展開されます。
16/// 括弧やイコールなどのプログラミング記号が含まれる場合は、すでに有効な Aura スクリプトと
17/// みなしてエイリアス展開をスキップし、入力をそのまま返します。
18///
19/// # 引数
20/// * `cmd_line` - 展開前のコマンドライン文字列
21///
22/// # 戻り値
23/// エイリアス展開後の Aura スクリプト文字列
24fn expand_aliases(cmd_line: &str) -> String {
25    let trimmed = cmd_line.trim();
26    if trimmed.is_empty() {
27        return String::new();
28    }
29
30    // プログラミング記号が含まれている場合は、エイリアス展開をスキップしてそのまま返す
31    let has_symbols = trimmed
32        .chars()
33        .any(|c| c == '(' || c == ')' || c == '{' || c == '}' || c == '=' || c == ',');
34    if has_symbols {
35        return String::from(trimmed);
36    }
37
38    // スペース区切りの単純なトークンに分割
39    let tokens: Vec<&str> = trimmed.split_whitespace().collect();
40    if tokens.is_empty() {
41        return String::from(trimmed);
42    }
43
44    let cmd = tokens[0];
45    let args = &tokens[1..];
46
47    match cmd {
48        "ls" => {
49            if args.is_empty() {
50                String::from("fs.ls()")
51            } else {
52                alloc::format!("fs.ls(\"{}\")", args[0])
53            }
54        }
55        "cd" => {
56            if args.is_empty() {
57                String::from("fs.cd(\"/user/\")")
58            } else {
59                alloc::format!("fs.cd(\"{}\")", args[0])
60            }
61        }
62        "pwd" => String::from("fs.pwd()"),
63        "cat" => {
64            if args.is_empty() {
65                String::from("print(\"USAGE: cat <file>\")")
66            } else {
67                alloc::format!("fs.cat(\"{}\")", args[0])
68            }
69        }
70        "mkdir" => {
71            if args.is_empty() {
72                String::from("print(\"USAGE: mkdir <dir>\")")
73            } else {
74                alloc::format!("fs.mkdir(\"{}\")", args[0])
75            }
76        }
77        "rm" => {
78            if args.is_empty() {
79                String::from("print(\"USAGE: rm <file>\")")
80            } else {
81                alloc::format!("fs.rm(\"{}\")", args[0])
82            }
83        }
84        "help" => String::from("help()"),
85        "apps" => String::from("os.apps()"),
86        "run" => {
87            if args.is_empty() {
88                String::from("print(\"USAGE: run <app>\")")
89            } else {
90                alloc::format!("os.run(\"{}\")", args[0])
91            }
92        }
93        // `drive <sub> [arg]` → drive.<sub>([arg]) (Google Drive CUI。認証は drive アプリと共有)
94        "drive" => match args.first().copied() {
95            Some("ls") => match args.get(1) {
96                Some(folder) => alloc::format!("drive.ls(\"{}\")", folder),
97                None => String::from("drive.ls()"),
98            },
99            Some("cat") if args.len() > 1 => alloc::format!("drive.cat(\"{}\")", args[1]),
100            Some("download") if args.len() > 2 => {
101                alloc::format!("drive.download(\"{}\", \"{}\")", args[1], args[2])
102            }
103            Some("rm") if args.len() > 1 => alloc::format!("drive.rm(\"{}\")", args[1]),
104            _ => String::from(
105                "print(\"USAGE: drive ls [folder_id] | drive cat <file_id> | drive download <file_id> <local_name> | drive rm <file_id>\")",
106            ),
107        },
108        // `gh <sub> [arg]` → gh.<sub>([arg]) (GitHub CUI。認証は gh_auth.json)
109        "gh" => match args.first().copied() {
110            Some("user") => String::from("gh.user()"),
111            Some("repos") => String::from("gh.repos()"),
112            Some("poll") => String::from("gh.poll()"),
113            Some("logout") => String::from("gh.logout()"),
114            Some("login") if args.len() > 1 => alloc::format!("gh.login(\"{}\")", args[1]),
115            _ => String::from(
116                "print(\"USAGE: gh login <client_id> | gh poll | gh user | gh repos | gh logout\")",
117            ),
118        },
119        _ => {
120            // アプリ名単体(例:settings)でインストールされている場合は、自動実行に展開する
121            let app_name = cmd;
122            let manifest_path = alloc::format!("/apps/{}/manifest.json", app_name);
123            if crate::kernel::fs::get_fs()
124                .read_file(&manifest_path)
125                .is_some()
126            {
127                if args.is_empty() {
128                    alloc::format!("os.run(\"{}\")", app_name)
129                } else {
130                    let mut run_cmd = alloc::format!("os.run(\"{}\"", app_name);
131                    for arg in args {
132                        run_cmd.push_str(&alloc::format!(", \"{}\"", arg));
133                    }
134                    run_cmd.push(')');
135                    run_cmd
136                }
137            } else {
138                String::from(trimmed)
139            }
140        }
141    }
142}
143
144/// ターミナルから入力されたコマンドラインを実行します。
145///
146/// この関数は、以下の処理を行います。
147/// 1. `clear` コマンドが入力された場合、ターミナルログをクリアして即時リターンします。
148/// 2. `history` または `h` コマンドが入力された場合、ターミナルの履歴を表示して即時リターンします。
149/// 3. それ以外のコマンドは、[`expand_aliases`] を通して Aura スクリプトへ変換し、非同期実行キューへ投入します。
150///
151/// # 引数
152/// * `cmd_line` - 実行するコマンドライン文字列
153/// * `terminal` - 操作対象のターミナル構造体へのミュータブル参照
154/// * `_kbd` - キーボード状態へのミュータブル参照 (未使用)
155pub fn execute_shell_command(cmd_line: &str, terminal: &mut Terminal, _kbd: &mut Keyboard) {
156    let trimmed = cmd_line.trim();
157    if trimmed.is_empty() {
158        return;
159    }
160
161    // clearコマンドはUI側でその場で完結させる
162    if trimmed == "clear" {
163        terminal.clear_logs();
164        return;
165    }
166
167    // history/hコマンドはUI側でその場で完結させる
168    if trimmed == "history" || trimmed == "h" {
169        crate::info!("[Terminal] Executing local history command: {}", trimmed);
170        terminal.show_history();
171        return;
172    }
173
174    // h(番号) でヒストリの指定番号を再実行
175    if let Some(rest) = trimmed.strip_prefix("h(").and_then(|s| s.strip_suffix(')')) {
176        match rest.trim().parse::<usize>() {
177            Ok(0) | Err(_) => {
178                terminal.print_line(String::from("  h(番号): 1 以上の数値を指定してください"));
179            }
180            Ok(n) => match terminal.history.get(n - 1).cloned() {
181                None => {
182                    terminal.print_line(alloc::format!("  h({}): ヒストリにありません(範囲: 1–{})", n, terminal.history.len()));
183                }
184                Some(cmd) => {
185                    terminal.print_line(alloc::format!("  → {}", cmd));
186                    if terminal.history.last() != Some(&cmd) {
187                        terminal.history.push(cmd.clone());
188                    }
189                    terminal.history_index = terminal.history.len();
190                    if let Some(ctx) = &terminal.context {
191                        ctx.cmd_queue.push(expand_aliases(&cmd));
192                    }
193                }
194            },
195        }
196        return;
197    }
198
199    let resolved_cmd = expand_aliases(trimmed);
200    if resolved_cmd.is_empty() {
201        return;
202    }
203
204    // 非同期Aura実行スレッドのキューへ送信
205    if let Some(ctx) = &terminal.context {
206        ctx.cmd_queue.push(resolved_cmd);
207    }
208}