Skip to content

Commit 2de53db

Browse files
committed
feat: tui
Signed-off-by: lxl66566 <lxl66566@gmail.com>
1 parent 90a5144 commit 2de53db

File tree

2 files changed

+240
-1
lines changed

2 files changed

+240
-1
lines changed

src/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod asset;
22
pub mod cli;
33
pub mod device;
44
pub mod log;
5+
pub mod tui;
56
pub mod utils;
67

78
use std::{env, fs, process};
@@ -12,11 +13,19 @@ use crate::{
1213
cli::{Cli, Commands},
1314
device::{DeviceManager, DeviceType},
1415
};
16+
use ::log::{debug, info};
1517

1618
fn main() -> anyhow::Result<()> {
1719
log::log_init();
1820

19-
let cli = Cli::parse();
21+
let cli = if std::env::args().len() > 1 {
22+
Cli::parse()
23+
} else {
24+
info!("TUI mode");
25+
tui::run_tui()?
26+
};
27+
28+
info!("args: {:?}", cli);
2029

2130
match cli.command {
2231
Commands::ListDevices => {
@@ -46,6 +55,7 @@ fn main() -> anyhow::Result<()> {
4655
args.speed,
4756
env::current_dir()?,
4857
)?;
58+
debug!("extracted files: {extracted:?}");
4959
let mut device_manager = DeviceManager::default();
5060
device_manager.select_device(DeviceType::Input, args.input_device)?;
5161
device_manager.select_device(DeviceType::Output, args.output_device)?;

src/tui.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
use anyhow::{Context, Result};
2+
use cpal::traits::{DeviceTrait, HostTrait};
3+
use std::fs;
4+
use terminal_menu::{
5+
TerminalMenuItem, back_button, button, label, list, menu, mut_menu, run, submenu,
6+
};
7+
8+
// 导入您在 main.rs 中定义的 pub clap 结构体
9+
use crate::cli::*;
10+
11+
/// 设备类型,用于区分输入和输出
12+
#[derive(Clone, Copy)]
13+
pub enum DeviceType {
14+
Input,
15+
Output,
16+
}
17+
18+
impl std::fmt::Display for DeviceType {
19+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20+
match self {
21+
DeviceType::Input => write!(f, "输入"),
22+
DeviceType::Output => write!(f, "输出"),
23+
}
24+
}
25+
}
26+
27+
/// 非交互式地获取可用设备名称列表
28+
fn get_device_names(device_type: DeviceType) -> Result<Vec<String>> {
29+
let host = cpal::default_host();
30+
let devices = host.devices().context("无法获取音频设备列表")?;
31+
32+
let names = devices
33+
.filter_map(|d| {
34+
let config_ok = match device_type {
35+
DeviceType::Input => d.default_input_config().is_ok(),
36+
DeviceType::Output => d.default_output_config().is_ok(),
37+
};
38+
if config_ok { d.name().ok() } else { None }
39+
})
40+
.collect::<Vec<_>>();
41+
42+
if names.is_empty() {
43+
anyhow::bail!("未找到可用的音频{}设备。", device_type);
44+
}
45+
46+
Ok(names)
47+
}
48+
49+
/// TUI 主函数,引导用户选择命令和参数,并返回一个完整的 Cli 对象
50+
pub fn run_tui() -> Result<Cli> {
51+
// 1. 在启动菜单前,预先获取所有需要的数据
52+
let input_devices = get_device_names(DeviceType::Input)?;
53+
let output_devices = get_device_names(DeviceType::Output)?;
54+
let executable_options = exec_options();
55+
56+
// 2. 构建菜单
57+
let main_menu = menu(vec![
58+
label("请选择一个要执行的操作,按 q 退出:"),
59+
button("ListDevices (列出设备)"),
60+
submenu("UnpackDll (解压DLL)", unpack_dll_menu()),
61+
submenu(
62+
"Start (启动程序)",
63+
start_menu(&input_devices, &output_devices, &executable_options),
64+
),
65+
submenu(
66+
"UnpackAndStart (解压并启动)",
67+
unpack_and_start_menu(&input_devices, &output_devices, &executable_options),
68+
),
69+
button("Exit (退出)"),
70+
]);
71+
72+
run(&main_menu);
73+
74+
if mut_menu(&main_menu).canceled() {
75+
anyhow::bail!("用户取消操作。");
76+
}
77+
78+
// 3. 根据用户的选择,构建并返回 Cli 对象
79+
let mut tmp = mut_menu(&main_menu);
80+
let chosen_command_name = tmp.selected_item_name();
81+
let command = match chosen_command_name {
82+
"ListDevices (列出设备)" => anyhow::Ok(Commands::ListDevices),
83+
"UnpackDll (解压DLL)" => {
84+
let sub_menu = tmp.get_submenu("UnpackDll (解压DLL)");
85+
if sub_menu.canceled() {
86+
anyhow::bail!("用户在 UnpackDll 菜单中取消了操作。");
87+
}
88+
Ok(Commands::UnpackDll(UnpackDllArgs {
89+
win64: sub_menu.selection_value("选择平台") == "win64",
90+
speed: sub_menu.selection_value("选择速度").parse()?,
91+
}))
92+
}
93+
"Start (启动程序)" => {
94+
let sub_menu = tmp.get_submenu("Start (启动程序)");
95+
if sub_menu.canceled() {
96+
anyhow::bail!("用户在 Start 菜单中取消了操作。");
97+
}
98+
99+
let selected_input_name = sub_menu.selection_value("选择输入设备");
100+
let input_device_index = input_devices
101+
.iter()
102+
.position(|r| r == selected_input_name)
103+
.context("选择的输入设备无效")?;
104+
105+
let selected_output_name = sub_menu.selection_value("选择输出设备");
106+
let output_device_index = output_devices
107+
.iter()
108+
.position(|r| r == selected_output_name)
109+
.context("选择的输出设备无效")?;
110+
111+
let exec_selection = sub_menu.selection_value("执行程序 (可选)");
112+
let exec = if exec_selection == "None" {
113+
None
114+
} else {
115+
Some(exec_selection.to_string())
116+
};
117+
118+
Ok(Commands::Start(StartArgs {
119+
input_device: input_device_index,
120+
output_device: output_device_index,
121+
speed: sub_menu.selection_value("选择速度").parse()?,
122+
exec,
123+
}))
124+
}
125+
"UnpackAndStart (解压并启动)" => {
126+
let sub_menu = tmp.get_submenu("UnpackAndStart (解压并启动)");
127+
if sub_menu.canceled() {
128+
anyhow::bail!("用户在 UnpackAndStart 菜单中取消了操作。");
129+
}
130+
131+
let selected_input_name = sub_menu.selection_value("选择输入设备");
132+
let input_device_index = input_devices
133+
.iter()
134+
.position(|r| r == selected_input_name)
135+
.context("选择的输入设备无效")?;
136+
137+
let selected_output_name = sub_menu.selection_value("选择输出设备");
138+
let output_device_index = output_devices
139+
.iter()
140+
.position(|r| r == selected_output_name)
141+
.context("选择的输出设备无效")?;
142+
143+
let exec_selection = sub_menu.selection_value("执行程序 (可选)");
144+
let exec = if exec_selection == "None" {
145+
None
146+
} else {
147+
Some(exec_selection.to_string())
148+
};
149+
150+
Ok(Commands::UnpackAndStart(UnpackAndStartArgs {
151+
win64: sub_menu.selection_value("选择平台") == "win64",
152+
input_device: input_device_index,
153+
output_device: output_device_index,
154+
speed: sub_menu.selection_value("选择速度").parse()?,
155+
exec,
156+
}))
157+
}
158+
_ => anyhow::bail!("用户退出了程序。"),
159+
}?;
160+
161+
Ok(Cli { command })
162+
}
163+
164+
/// 'UnpackDll' 命令的子菜单
165+
fn unpack_dll_menu() -> Vec<TerminalMenuItem> {
166+
vec![
167+
label("配置 UnpackDll 命令参数"),
168+
list("选择平台", vec!["win32", "win64"]),
169+
list("选择速度", speed_options()),
170+
button("确认!"),
171+
back_button("Back (返回)"),
172+
]
173+
}
174+
175+
/// 'Start' 命令的子菜单
176+
fn start_menu<'a>(
177+
input_devices: &'a [String],
178+
output_devices: &'a [String],
179+
executables: &'a [String],
180+
) -> Vec<TerminalMenuItem> {
181+
vec![
182+
label("配置 Start 命令参数"),
183+
list("选择输入设备", input_devices.to_vec()),
184+
list("选择输出设备", output_devices.to_vec()),
185+
list("选择速度", speed_options()),
186+
list("执行程序 (可选)", executables.to_vec()),
187+
button("确认!"),
188+
back_button("Back (返回)"),
189+
]
190+
}
191+
192+
/// 'UnpackAndStart' 命令的子菜单
193+
fn unpack_and_start_menu<'a>(
194+
input_devices: &'a [String],
195+
output_devices: &'a [String],
196+
executables: &'a [String],
197+
) -> Vec<TerminalMenuItem> {
198+
vec![
199+
label("配置 UnpackAndStart 命令参数"),
200+
list("选择平台", vec!["win32", "win64"]),
201+
list("选择输入设备", input_devices.to_vec()),
202+
list("选择输出设备", output_devices.to_vec()),
203+
list("选择速度", speed_options()),
204+
list("执行程序 (可选)", executables.to_vec()),
205+
button("确认!"),
206+
back_button("Back (返回)"),
207+
]
208+
}
209+
210+
/// 生成速度选项 (1.0 ~ 2.5)
211+
fn speed_options() -> Vec<&'static str> {
212+
vec![
213+
"1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "1.9", "2.0", "2.1", "2.2",
214+
"2.3", "2.4", "2.5",
215+
]
216+
}
217+
218+
/// 获取当前目录下的文件和文件夹作为 `exec` 的选项
219+
fn exec_options() -> Vec<String> {
220+
let mut options = vec!["None".to_string()]; // 提供不选择的选项
221+
if let Ok(entries) = fs::read_dir(".") {
222+
for entry in entries.flatten() {
223+
if let Some(name) = entry.file_name().to_str() {
224+
options.push(name.to_string());
225+
}
226+
}
227+
}
228+
options
229+
}

0 commit comments

Comments
 (0)