Skip to content

Commit e091c77

Browse files
committed
feat(catalog): add managed LazyVim and YouTube players
1 parent e9e8f35 commit e091c77

15 files changed

Lines changed: 352 additions & 31 deletions

docs/plans/completion-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Date: 2026-07-17
66

77
| Requirement | Authoritative evidence | Result |
88
| --- | --- | --- |
9-
| Engineering baseline | `scripts/gates/run_all.sh`, CI workflow, 102 default tests, Clippy warnings denied | Pass |
9+
| Engineering baseline | `scripts/gates/run_all.sh`, CI workflow, 105 default tests, Clippy warnings denied | Pass |
1010
| Pack-first app navigation and responsive rendering | `tests/tui_state.rs`, real single-app tmux lifecycle, 120x30 release PTY smoke | Pass |
1111
| Installation execution and recovery | `tests/install_execution.rs`, `tests/queue_state.rs`, `tests/storage_state.rs` | Pass |
1212
| Full Linux catalog install and launch | Local package/source/dependency live gate plus manual `catalog-install-gate.yml`, isolated Ubuntu runner and evidence per app | Source and dependency gate pass; full install matrix not yet run |

docs/spec/v0.1/04-starter-catalog.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
## 4.1 Pack 구성(v0.1)
44
- Music Pack (6)
5-
- Video Pack (6)
5+
- Video Pack (8)
66
- Podcasts & Reading Pack (7)
77
- Information Search Pack (1)
88
- Files Pack (16)
99
- Fun Pack (9)
1010
- Games Pack (3)
1111
- Editors & IDEs Pack (4)
12-
= 합계 52개 + (옵션 Search-only) Agents(3) = 총 55
12+
= 합계 54개 + (옵션 Search-only) Agents(3) = 총 57
1313
v0.1 목표 40~80 범위 충족.
1414

1515
> NOTE: “패키지명/가용성”은 OS/배포판마다 달라, v0.1은 package_hints + resolver 기반으로 설치 성공률을 확보한다.
@@ -22,13 +22,15 @@ v0.1 목표 40~80 범위 충족.
2222
5) termusic
2323
6) mpv
2424

25-
## 4.3 Video Pack (6)
25+
## 4.3 Video Pack (8)
2626
1) mpv
2727
2) yewtube
28-
3) yt-dlp
29-
4) ffmpeg
30-
5) streamlink
31-
6) mediainfo
28+
3) youtube-tui
29+
4) tplay
30+
5) yt-dlp
31+
6) ffmpeg
32+
7) streamlink
33+
8) mediainfo
3234

3335
## 4.4 Podcasts & Reading Pack (7)
3436
1) shellcast
@@ -79,7 +81,7 @@ v0.1 목표 40~80 범위 충족.
7981
## 4.9 Editors & IDEs Pack (4)
8082
1) micro
8183
2) helix
82-
3) neovim
84+
3) lazyvim
8385
4) visidata
8486

8587
## 4.10 Search-only (옵션)

docs/spec/v0.1/06-agents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
## 6.5 Coding Desk 연결
2727
- Agent 실행 시 tmux/zellij workspace 열기
2828
- right-bottom pane에 선택 agent 실행
29-
- editor 우선순위: Neovim > Helix > Micro
29+
- editor 우선순위: LazyVim > Helix > Micro
3030

3131
## 6.6 v0.1 한계
3232
- 에이전트 내부 동작 강제 차단은 제공하지 않는다.

registry/catalog.yaml

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ packs:
66
- id: video-pack
77
title: Video Pack
88
exposure: starter
9-
tool_ids: [mpv, yewtube, yt-dlp, ffmpeg, streamlink, mediainfo]
9+
tool_ids: [mpv, yewtube, youtube-tui, tplay, yt-dlp, ffmpeg, streamlink, mediainfo]
1010
- id: podcasts-reading-pack
1111
title: Podcasts & Reading Pack
1212
exposure: starter
@@ -30,7 +30,7 @@ packs:
3030
- id: edit-pack
3131
title: Editors & IDEs Pack
3232
exposure: starter
33-
tool_ids: [micro, helix, neovim, visidata]
33+
tool_ids: [micro, helix, lazyvim, visidata]
3434
- id: agents-pack
3535
title: Agents Pack
3636
exposure: search_only
@@ -165,6 +165,47 @@ tools:
165165
package_hints: ["yewtube"]
166166
checks:
167167
- which: "yt"
168+
- id: youtube-tui
169+
name: "YouTube TUI"
170+
description: "Browse and search YouTube in a configurable TUI with external video playback."
171+
install_timeout_sec: 3600
172+
category: entertainment
173+
tags: []
174+
audience: general
175+
risk: SAFE
176+
exposure: starter
177+
run:
178+
cmd: "youtube-tui"
179+
installers:
180+
- platform: macos
181+
method: cargo
182+
package_hints: ["youtube-tui"]
183+
- platform: linux
184+
method: cargo
185+
package_hints: ["youtube-tui"]
186+
system_packages: ["pkg-config", "libmpv-dev", "libsixel-dev", "libxcb1-dev", "mpv", "yt-dlp"]
187+
- id: tplay
188+
name: "tplay"
189+
description: "Play local or online media, including YouTube, as colored terminal ASCII art."
190+
install_timeout_sec: 3600
191+
category: entertainment
192+
tags: []
193+
audience: general
194+
risk: SAFE
195+
exposure: starter
196+
run:
197+
cmd: "tplay"
198+
launch_argument:
199+
label: "Media URL or local path"
200+
placeholder: "https://www.youtube.com/watch?v=..."
201+
installers:
202+
- platform: macos
203+
method: cargo
204+
package_hints: ["tplay"]
205+
- platform: linux
206+
method: cargo
207+
package_hints: ["tplay"]
208+
system_packages: ["libssl-dev", "ffmpeg", "libavfilter-dev", "libavdevice-dev", "libavformat-dev", "libavcodec-dev", "libswscale-dev", "libasound2-dev", "yt-dlp", "libmpv-dev", "clang"]
168209
- id: yt-dlp
169210
name: "yt-dlp"
170211
category: entertainment
@@ -945,23 +986,25 @@ tools:
945986
- platform: linux
946987
method: snap_classic
947988
package_hints: ["helix"]
948-
- id: neovim
949-
name: "Neovim"
950-
description: "An extensible modal text editor for code and general editing."
951-
category: edit
989+
- id: lazyvim
990+
name: "LazyVim"
991+
description: "A full IDE configuration for Neovim, isolated from your existing Neovim setup."
992+
category: ide
952993
tags: []
953994
audience: developer
954995
risk: SAFE
955996
exposure: starter
956997
run:
957-
cmd: "nvim"
998+
cmd: "t4e-lazyvim"
999+
checks:
1000+
- which: "t4e-lazyvim"
9581001
installers:
9591002
- platform: macos
960-
method: brew
961-
package_hints: ["neovim"]
1003+
method: lazyvim
1004+
package_hints: ["lazyvim"]
9621005
- platform: linux
963-
method: apt
964-
package_hints: ["neovim"]
1006+
method: lazyvim
1007+
package_hints: ["lazyvim"]
9651008
- id: visidata
9661009
name: "VisiData"
9671010
description: "Interactively explore, transform, and analyze tabular data."

src/app/state.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ pub struct LaunchOptionsState {
7676
pub selections: Vec<LaunchOptionSelection>,
7777
}
7878

79+
#[derive(Debug, Clone, PartialEq, Eq)]
80+
pub struct LaunchArgumentState {
81+
pub tool_id: String,
82+
pub tool_name: String,
83+
pub label: String,
84+
pub placeholder: String,
85+
pub input: String,
86+
}
87+
7988
#[derive(Debug, Clone, PartialEq, Eq)]
8089
pub enum AppInput {
8190
Text(String),
@@ -162,6 +171,7 @@ pub struct AppState {
162171
pub workspace_missing_tools: Vec<String>,
163172
pub app_view: Option<AppViewState>,
164173
pub link_picker: Option<LinkPickerState>,
174+
pub launch_argument: Option<LaunchArgumentState>,
165175
pub launch_options: Option<LaunchOptionsState>,
166176
pending_tool_launch: Option<ToolLaunchRequest>,
167177
return_after_app_close: bool,
@@ -246,6 +256,7 @@ impl AppState {
246256
workspace_missing_tools: Vec::new(),
247257
app_view: None,
248258
link_picker: None,
259+
launch_argument: None,
249260
launch_options: None,
250261
pending_tool_launch: None,
251262
return_after_app_close: false,
@@ -288,6 +299,11 @@ impl AppState {
288299
return;
289300
}
290301

302+
if self.launch_argument.is_some() {
303+
self.handle_launch_argument_key(key.code);
304+
return;
305+
}
306+
291307
if self.launch_options.is_some() {
292308
self.handle_launch_options_key(key.code);
293309
return;
@@ -343,6 +359,7 @@ impl AppState {
343359
if !self.mouse_enabled
344360
|| self.confirmation.is_some()
345361
|| self.ai_confirmation.is_some()
362+
|| self.launch_argument.is_some()
346363
|| self.launch_options.is_some()
347364
|| self.uninstall_confirmation.is_some()
348365
|| self.search_mode
@@ -1352,6 +1369,7 @@ impl AppState {
13521369
let tool_id = tool.id.clone();
13531370
let tool_name = tool.name.clone();
13541371
let command = tool.run_command_for_current_platform().to_string();
1372+
let launch_argument = tool.launch_argument.clone();
13551373
let options = tool.run_options.clone();
13561374
if let Some(index) = self
13571375
.app_view
@@ -1367,6 +1385,17 @@ impl AppState {
13671385
self.status = format!("Returned to {tool_name}");
13681386
return;
13691387
}
1388+
if let Some(argument) = launch_argument {
1389+
self.launch_argument = Some(LaunchArgumentState {
1390+
tool_id,
1391+
tool_name,
1392+
label: argument.label,
1393+
placeholder: argument.placeholder,
1394+
input: String::new(),
1395+
});
1396+
self.status = "Enter the required launch value".to_string();
1397+
return;
1398+
}
13701399
if options.is_empty() {
13711400
self.effects
13721401
.push_back(AppEffect::LaunchTool(ToolLaunchRequest {
@@ -1396,6 +1425,56 @@ impl AppState {
13961425
self.status = "Configure launch options".to_string();
13971426
}
13981427

1428+
fn handle_launch_argument_key(&mut self, code: KeyCode) {
1429+
match code {
1430+
KeyCode::Esc => {
1431+
self.launch_argument = None;
1432+
self.status = "Launch cancelled".to_string();
1433+
}
1434+
KeyCode::Backspace => {
1435+
if let Some(argument) = &mut self.launch_argument {
1436+
argument.input.pop();
1437+
}
1438+
}
1439+
KeyCode::Char(ch) => {
1440+
if let Some(argument) = &mut self.launch_argument {
1441+
argument.input.push(ch);
1442+
}
1443+
}
1444+
KeyCode::Enter => {
1445+
let Some(argument) = self.launch_argument.take() else {
1446+
return;
1447+
};
1448+
let value = argument.input.trim();
1449+
if value.is_empty() {
1450+
self.launch_argument = Some(argument);
1451+
self.status = "A launch value is required".to_string();
1452+
return;
1453+
}
1454+
let Some(tool) = self
1455+
.catalog
1456+
.tools
1457+
.iter()
1458+
.find(|tool| tool.id == argument.tool_id)
1459+
else {
1460+
self.status = "Launch tool is no longer available".to_string();
1461+
return;
1462+
};
1463+
self.effects
1464+
.push_back(AppEffect::LaunchTool(ToolLaunchRequest {
1465+
tool_id: tool.id.clone(),
1466+
command: format!(
1467+
"{} {}",
1468+
tool.run_command_for_current_platform(),
1469+
shell_quote(value)
1470+
),
1471+
}));
1472+
self.status = format!("Opening {}", tool.name);
1473+
}
1474+
_ => {}
1475+
}
1476+
}
1477+
13991478
fn handle_launch_options_key(&mut self, code: KeyCode) {
14001479
let option_count = self
14011480
.launch_options
@@ -1979,6 +2058,7 @@ fn uninstall_command(method: &InstallMethod, package: &str) -> Option<String> {
19792058
InstallMethod::Pipx => format!("pipx uninstall {package}"),
19802059
InstallMethod::NpmGlobal => format!("npm uninstall --global {package}"),
19812060
InstallMethod::Cargo => format!("cargo uninstall {package}"),
2061+
InstallMethod::LazyVim => "rm -f \"$HOME/.local/bin/t4e-lazyvim\" && rm -rf \"${XDG_CONFIG_HOME:-$HOME/.config}/t4e-lazyvim\" \"${XDG_DATA_HOME:-$HOME/.local/share}/t4e-lazyvim\" \"${XDG_STATE_HOME:-$HOME/.local/state}/t4e-lazyvim\" \"${XDG_CACHE_HOME:-$HOME/.cache}/t4e-lazyvim\"".to_string(),
19822062
InstallMethod::Go | InstallMethod::Script | InstallMethod::Other => return None,
19832063
};
19842064
Some(command)
@@ -2033,6 +2113,10 @@ fn app_input_from_key(key: KeyEvent) -> Option<AppInput> {
20332113
))
20342114
}
20352115

2116+
fn shell_quote(value: &str) -> String {
2117+
format!("'{}'", value.replace('\'', "'\"'\"'"))
2118+
}
2119+
20362120
fn extract_urls(content: &str) -> Vec<String> {
20372121
let mut urls = Vec::new();
20382122
let mut offset = 0;

src/app/terminal.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ fn process_effects(
309309
if active.contains_key(&tool_id) {
310310
continue;
311311
}
312-
if install_method_requires_privileges(&request.method)
312+
if uninstall_method_requires_privileges(&request.method)
313313
&& let Err(error) = session.suspend_for(|| acquire_install_privileges(&tool_id))
314314
{
315315
app.apply_uninstall_result(&tool_id, false, &error.to_string());
@@ -561,6 +561,19 @@ fn reload_app_view(app: &mut AppState, tmux: &TmuxRuntime<SystemTmuxRunner>) {
561561
}
562562

563563
fn install_method_requires_privileges(method: &InstallMethod) -> bool {
564+
matches!(
565+
method,
566+
InstallMethod::Apt
567+
| InstallMethod::Dnf
568+
| InstallMethod::Pacman
569+
| InstallMethod::Snap
570+
| InstallMethod::SnapClassic
571+
| InstallMethod::Pipx
572+
| InstallMethod::LazyVim
573+
)
574+
}
575+
576+
fn uninstall_method_requires_privileges(method: &InstallMethod) -> bool {
564577
matches!(
565578
method,
566579
InstallMethod::Apt
@@ -705,7 +718,10 @@ impl Drop for TerminalSession {
705718

706719
#[cfg(test)]
707720
mod tests {
708-
use super::{frame_poll_interval, install_method_requires_privileges};
721+
use super::{
722+
frame_poll_interval, install_method_requires_privileges,
723+
uninstall_method_requires_privileges,
724+
};
709725
use crate::app::events::Screen;
710726
use crate::catalog::models::InstallMethod;
711727
use std::time::Duration;
@@ -732,7 +748,11 @@ mod tests {
732748
&InstallMethod::SnapClassic
733749
));
734750
assert!(install_method_requires_privileges(&InstallMethod::Pipx));
751+
assert!(install_method_requires_privileges(&InstallMethod::LazyVim));
735752
assert!(!install_method_requires_privileges(&InstallMethod::Brew));
736753
assert!(!install_method_requires_privileges(&InstallMethod::Cargo));
754+
assert!(!uninstall_method_requires_privileges(
755+
&InstallMethod::LazyVim
756+
));
737757
}
738758
}

0 commit comments

Comments
 (0)