A Swift package that brings Vim-style navigation and powerful terminal UI capabilities to your command-line applications. Create interactive terminal UIs with ease, supporting both single-column layouts (like file explorers) and multi-column layouts (like menus).
- 🎮 Vim-style (hjkl) and arrow key navigation
- 📊 Flexible single and multi-column layouts
- 🔄 Loading animations and state management
- ⌨️ Raw terminal mode handling
- 🎨 Terminal control sequences
- 🔧 Async operation support
Add VimTerminalKit to your Package.swift
:
dependencies: [
.package(url: "https://github.com/marcusziade/VimTerminalKit.git", from: "1.0.0")
]
Then import it in your source files:
import VimTerminalKit
Best for:
- File explorers
- Linear lists
- Command logs
- Vertical menus
Required setup:
let navigator = VimTerminalKit.Navigator(
itemCount: items.count,
columnsCount: 1 // Must be 1 for vertical lists
)
Best for:
- Dashboard layouts
- Option grids
- Category selections
- Side-by-side views
Required setup:
let navigator = VimTerminalKit.Navigator(
itemCount: items.count,
columnsCount: 2 // 2 or more for grid layouts
)
The correct initialization pattern is crucial for avoiding compiler errors and runtime issues:
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()
}
}
}
The StateManager handles UI updates and loading states:
// Initialize with UI update callback
let stateManager = VimTerminalKit.StateManager {
// Update UI here
}
// Show loading state
await stateManager.withLoading(message: "Loading...") {
try await someAsyncWork()
}
Handle navigation inputs:
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
}
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()
}
}
}
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 ...
}
}
- ❌ Don't use
self
in property initializers - ❌ Don't set up callbacks directly in property initialization
- ❌ Don't access properties before they're initialized
- ✅ Initialize properties first, then set up callbacks
- ✅ Use proper initialization order
- ✅ Set up complex logic in separate setup methods
- ❌ Don't forget to update navigator when items change
- ❌ Don't mix column counts (stick to either 1 or 2+)
- ✅ Always update navigator item count when content changes
- ✅ Use columnsCount: 1 for file explorers
- ✅ Use columnsCount: 2+ for grid layouts
// 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()
}
-
Setup and Cleanup
- Always call
VimTerminalKit.setup()
before starting - Always use
defer { VimTerminalKit.cleanup() }
after setup
- Always call
-
Memory Management
- Use
[weak self]
in closures - Clean up resources when app terminates
- Use
-
Error Handling
- Handle all async operations appropriately
- Provide user feedback during errors
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature
) - Commit your changes (
git commit -m 'Add some AmazingFeature'
) - Push to the branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
Please read our Contributing Guidelines before submitting pull requests.
This project is licensed under the MIT License - see the LICENSE file for details.
- Inspired by Vim's navigation system
- Built with Swift's modern concurrency features