VimTerminalKit

VimTerminalKit-logo-small

一个 Swift 包,为你的命令行应用程序带来 Vim 风格的导航和强大的终端 UI 功能。轻松创建交互式终端 UI,支持单列布局(如文件浏览器)和多列布局(如菜单)。

功能

安装

Swift 包管理器

将 VimTerminalKit 添加到你的 Package.swift

dependencies: [
    .package(url: "https://github.com/marcusziade/VimTerminalKit.git", from: "1.0.0")
]

然后在你的源文件中导入它

import VimTerminalKit

布局类型

单列布局(文件浏览器)

最佳应用场景

必要设置

let navigator = VimTerminalKit.Navigator(
    itemCount: items.count,
    columnsCount: 1  // Must be 1 for vertical lists
)

多列布局(网格菜单)

最佳应用场景

必要设置

let navigator = VimTerminalKit.Navigator(
    itemCount: items.count,
    columnsCount: 2  // 2 or more for grid layouts
)

核心概念

1. 正确的初始化

正确的初始化模式对于避免编译器错误和运行时问题至关重要。

final class MyApp {
    private let navigator: VimTerminalKit.Navigator
    private let stateManager: VimTerminalKit.StateManager
    private var items: [String] = []
    
    init() {
        // 1. Initialize basic properties first
        self.items = []
        // 2. Set up navigator with initial state
        self.navigator = .init(itemCount: 1, columnsCount: 1)
        // 3. Initialize stateManager with empty closure
        self.stateManager = .init { }
        // 4. Set up callbacks after initialization
        setupStateManager()
    }
    
    private func setupStateManager() {
        stateManager = .init { [weak self] in
            self?.updateUI()
        }
    }
}

2. 状态管理

StateManager 处理 UI 更新和加载状态。

// Initialize with UI update callback
let stateManager = VimTerminalKit.StateManager { 
    // Update UI here
}

// Show loading state
await stateManager.withLoading(message: "Loading...") {
    try await someAsyncWork()
}

3. 导航控制

处理导航输入

switch VimTerminalKit.InputReader.getInput() {
case .vim(let direction), .arrow(let direction):
    navigator.navigate(.arrow(direction))
case .enter:
    // Handle selection
case .quit:
    isRunning = false
default:
    break
}

完整示例

1. 查看/Sources 下名为 'FileExplorer' 的完整 CLI 应用程序示例

CleanShot 2024-10-31 at 20 24 41

2. 文件浏览器实现

final class Explorer {
    private let fileManager = FileManager.default
    private var currentPath: String
    private var items: [FileItem] = []
    private var navigator: VimTerminalKit.Navigator
    private var stateManager: VimTerminalKit.StateManager
    private var isRunning = true
    private var pathHistory: [String] = []

    init() {
        // IMPORTANT: Order matters for initialization
        self.currentPath = fileManager.currentDirectoryPath
        self.navigator = .init(itemCount: 1, columnsCount: 1)
        self.stateManager = .init { }
        setupStateManager()
    }

    private func setupStateManager() {
        stateManager = .init { [weak self] in
            self?.clearScreen()
            self?.printInterface()
        }
    }

    private func loadCurrentDirectory() {
        Task { [weak self] in
            guard let self else { return }
            try await self.stateManager.withLoading(message: "Loading...") {
                let contents = try self.fileManager.contentsOfDirectory(atPath: self.currentPath)
                self.items = // ... process contents ...
                let totalItems = self.currentPath == "/" ? items.count : items.count + 1
                self.navigator = .init(itemCount: totalItems, columnsCount: 1)
            }
        }
    }

    func start() {
        VimTerminalKit.setup()
        defer { VimTerminalKit.cleanup() }
        
        loadCurrentDirectory()
        
        while isRunning {
            clearScreen()
            printInterface()
            handleInput()
        }
    }
}

3. 菜单实现

struct MenuApp {
    private let navigator: VimTerminalKit.Navigator
    private let stateManager: VimTerminalKit.StateManager
    private var isRunning = true
    private let menuItems = ["Option 1", "Option 2", "Option 3", "Option 4"]
    
    init() {
        self.navigator = .init(itemCount: menuItems.count, columnsCount: 2)
        self.stateManager = .init { }
        setupStateManager()
    }
    
    private func setupStateManager() {
        stateManager = .init { [weak self] in
            self?.redrawInterface()
        }
    }
    
    func start() {
        VimTerminalKit.setup()
        defer { VimTerminalKit.cleanup() }
        
        while isRunning {
            redrawInterface()
            handleInput()
        }
    }
    
    private func redrawInterface() {
        // ... draw menu interface ...
    }
}

常见陷阱

初始化问题

导航问题

高级功能

终端控制

// Screen control
print(VimTerminalKit.Terminal.Control.clearScreen)

// Cursor control
print(VimTerminalKit.Terminal.Control.hideCursor)
print(VimTerminalKit.Terminal.Control.showCursor)

// Movement
print(VimTerminalKit.Terminal.Control.up)
print(VimTerminalKit.Terminal.Control.down)

状态管理

// Progress indication
stateManager.startLoading(message: "Step 1")
// ... work ...
stateManager.updateLoadingMessage("Step 2")
// ... work ...
stateManager.stopLoading()

// Async operations
await stateManager.withLoading(message: "Processing...") {
    try await someAsyncWork()
}

最佳实践

  1. 设置和清理

    • 始终在启动前调用 VimTerminalKit.setup()
    • 始终在设置后使用 defer { VimTerminalKit.cleanup() }
  2. 内存管理

    • 在闭包中使用 [weak self]
    • 当应用程序终止时清理资源
  3. 错误处理

    • 适当处理所有异步操作
    • 在错误期间提供用户反馈

贡献

欢迎贡献!请按照以下步骤

  1. Fork 仓库
  2. 创建你的功能分支 (git checkout -b feature/AmazingFeature)
  3. 提交你的更改 (git commit -m 'Add some AmazingFeature')
  4. 推送到分支 (git push origin feature/AmazingFeature)
  5. 打开一个拉取请求

在提交拉取请求之前,请阅读我们的 贡献指南

许可证

本项目根据 MIT 许可证获得许可 - 有关详细信息,请参阅 LICENSE 文件。

致谢