✨ feat:初始化 QuickJump 项目,实现完整的目录跳转和配置管理功能
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dist-newstyle
|
||||
33
CHANGELOG.md
Normal file
33
CHANGELOG.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Revision history for quickjump
|
||||
|
||||
## 0.3.0.1 -- 2026-02-04
|
||||
|
||||
* 添加静默模式选项 (--quiet/-q),用于抑制输出消息
|
||||
* 改进 Windows 路径处理,支持带空格的路径
|
||||
* 优化配置文件管理,支持 XDG 配置目录
|
||||
* 增强错误处理和用户提示信息
|
||||
|
||||
## 0.3.0.0 -- 2026-02-03
|
||||
|
||||
* 新增快速操作命令 (quick/k),支持快速打开目录
|
||||
* 添加配置导出/导入功能,支持配置备份和迁移
|
||||
* 新增配置编辑功能,可直接使用编辑器打开配置文件
|
||||
* 添加默认路径设置,可配置默认打开的目录
|
||||
* 支持设置首选编辑器和文件管理器
|
||||
* 改进交互式选择模式,支持按编号或名称选择
|
||||
* 增强路径展开功能,支持环境变量和 ~ 符号
|
||||
|
||||
## 0.2.0.0 -- 2026-02-02
|
||||
|
||||
* 实现完整的目录跳转功能 (jump/j 命令)
|
||||
* 添加配置管理系统,支持添加、删除、列出跳转条目
|
||||
* 新增交互式选择模式 (--interactive/-i)
|
||||
* 实现配置文件持久化,使用 JSON 格式存储
|
||||
* 添加条目描述和优先级支持
|
||||
* 实现自动检测文件管理器功能,支持跨平台
|
||||
* 添加 Shell 集成功能,支持 Bash/Zsh 和 PowerShell
|
||||
* 实现 Tab 补全功能
|
||||
|
||||
## 0.1.0.0 -- 2026-02-02
|
||||
|
||||
* First version. Released on an unsuspecting world.
|
||||
20
LICENSE
Normal file
20
LICENSE
Normal file
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2026 Sidney Zhang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
336
README.md
Normal file
336
README.md
Normal file
@@ -0,0 +1,336 @@
|
||||
# QuickJump
|
||||
|
||||
一个使用 Haskell 编写的快速目录跳转命令行工具,支持配置管理、快速打开和导入导出功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **快速目录跳转** - 根据配置快速跳转到常用目录
|
||||
- **快速打开** - 使用文件管理器快速打开文件夹
|
||||
- **配置管理** - 添加、删除、修改配置条目
|
||||
- **导入导出** - 备份和分享配置
|
||||
- **Shell 集成** - 提供 Bash/Zsh/PowerShell 补全支持
|
||||
- **交互式选择** - 支持交互式选择跳转目标
|
||||
- **静默模式** - 抑制输出消息,适合脚本使用
|
||||
- **跨平台支持** - 支持 Windows、macOS 和 Linux
|
||||
|
||||
## 版本
|
||||
|
||||
当前版本: **0.3.0.1**
|
||||
|
||||
## 安装
|
||||
|
||||
### 使用 Cabal
|
||||
|
||||
```bash
|
||||
cabal build
|
||||
cabal install
|
||||
```
|
||||
|
||||
### 使用 Stack
|
||||
|
||||
```bash
|
||||
stack build
|
||||
stack install
|
||||
```
|
||||
|
||||
## 配置 Shell 集成
|
||||
|
||||
### Linux/macOS (Bash/Zsh)
|
||||
|
||||
将以下命令添加到你的 `.bashrc` 或 `.zshrc`:
|
||||
|
||||
```bash
|
||||
eval "$(quickjump shell-integration)"
|
||||
```
|
||||
|
||||
### Windows (PowerShell)
|
||||
|
||||
将以下命令添加到你的 PowerShell 配置文件中:
|
||||
|
||||
```powershell
|
||||
# 首先查看你的配置文件路径
|
||||
echo $PROFILE
|
||||
|
||||
# 如果文件不存在,创建它
|
||||
if (!(Test-Path $PROFILE)) { New-Item -Type File -Path $PROFILE -Force }
|
||||
|
||||
# 编辑配置文件
|
||||
notepad $PROFILE
|
||||
```
|
||||
|
||||
然后在配置文件中添加:
|
||||
|
||||
```powershell
|
||||
. (quickjump shell-integration | Out-String)
|
||||
```
|
||||
|
||||
这将启用:
|
||||
- `qj` 命令用于目录跳转
|
||||
- `qo` 命令用于快速打开
|
||||
- Tab 自动补全(PowerShell 也支持)
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 目录跳转
|
||||
|
||||
```bash
|
||||
# 跳转到配置中的条目
|
||||
quickjump jump home
|
||||
quickjump j home
|
||||
|
||||
# 使用 Shell 集成函数(需要先配置 Shell 集成)
|
||||
qj home
|
||||
|
||||
# 交互式选择跳转目标
|
||||
quickjump jump --interactive
|
||||
quickjump j -i
|
||||
|
||||
# 静默模式(不输出消息)
|
||||
quickjump jump home --quiet
|
||||
quickjump j home -q
|
||||
```
|
||||
|
||||
### 快速打开
|
||||
|
||||
```bash
|
||||
# 打开配置中的目录(使用文件管理器)
|
||||
quickjump quick open home
|
||||
quickjump k open home
|
||||
|
||||
# 使用 Shell 集成函数
|
||||
qo home
|
||||
|
||||
# 打开指定路径
|
||||
quickjump quick --path ~/Documents
|
||||
quickjump k -p ~/Documents
|
||||
|
||||
# 打开默认目录
|
||||
quickjump quick default
|
||||
quickjump k -d
|
||||
|
||||
# 列出所有快速目标
|
||||
quickjump quick list
|
||||
quickjump k -l
|
||||
|
||||
# 静默模式
|
||||
quickjump quick open home --quiet
|
||||
quickjump k open home -q
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
```bash
|
||||
# 添加新条目
|
||||
quickjump config add myproject ~/Projects/myproject
|
||||
quickjump config add work ~/Work --description "Work directory"
|
||||
quickjump c add myproject ~/Projects/myproject
|
||||
|
||||
# 列出所有条目
|
||||
quickjump config list
|
||||
quickjump config ls
|
||||
quickjump c list
|
||||
|
||||
# 删除条目
|
||||
quickjump config remove myproject
|
||||
quickjump config rm myproject
|
||||
quickjump c remove myproject
|
||||
|
||||
# 设置默认路径
|
||||
quickjump config set-default ~/Documents
|
||||
quickjump c set-default ~/Documents
|
||||
|
||||
# 设置首选编辑器
|
||||
quickjump config set-editor vim
|
||||
quickjump config set-editor "code --wait"
|
||||
quickjump c set-editor vim
|
||||
|
||||
# 设置首选文件管理器
|
||||
quickjump config set-file-manager nautilus # Linux
|
||||
quickjump config set-file-manager finder # macOS
|
||||
quickjump config set-file-manager explorer # Windows
|
||||
quickjump c set-file-manager explorer
|
||||
|
||||
# 编辑配置文件
|
||||
quickjump config edit
|
||||
quickjump c edit
|
||||
|
||||
# 显示当前配置
|
||||
quickjump config show
|
||||
quickjump c show
|
||||
|
||||
# 静默模式
|
||||
quickjump config add myproject ~/Projects/myproject --quiet
|
||||
quickjump c add myproject ~/Projects/myproject -q
|
||||
```
|
||||
|
||||
### 导入导出
|
||||
|
||||
```bash
|
||||
# 导出配置
|
||||
quickjump config export ~/backup/quickjump-config.json
|
||||
quickjump c export ~/backup/quickjump-config.json
|
||||
|
||||
# 导入配置(替换现有)
|
||||
quickjump config import ~/backup/quickjump-config.json
|
||||
quickjump c import ~/backup/quickjump-config.json
|
||||
|
||||
# 导入配置(合并)
|
||||
quickjump config import ~/backup/quickjump-config.json --merge
|
||||
quickjump config import ~/backup/quickjump-config.json -m
|
||||
quickjump c import ~/backup/quickjump-config.json -m
|
||||
```
|
||||
|
||||
### 其他命令
|
||||
|
||||
```bash
|
||||
# 显示版本信息
|
||||
quickjump --version
|
||||
quickjump -v
|
||||
|
||||
# 显示帮助信息
|
||||
quickjump --help
|
||||
quickjump -h
|
||||
|
||||
# 显示特定命令的帮助
|
||||
quickjump jump --help
|
||||
quickjump config --help
|
||||
```
|
||||
|
||||
## 配置文件
|
||||
|
||||
配置文件默认位于:
|
||||
|
||||
- **Linux/macOS**: `~/.config/quickjump/config.json`
|
||||
- **Windows**: `%APPDATA%\quickjump\config.json`
|
||||
|
||||
可以通过环境变量 `QUICKJUMP_CONFIG` 自定义配置文件路径:
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
export QUICKJUMP_CONFIG=/path/to/custom/config.json
|
||||
|
||||
# Windows PowerShell
|
||||
$env:QUICKJUMP_CONFIG = "C:\path\to\custom\config.json"
|
||||
```
|
||||
|
||||
### 配置格式
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"entries": {
|
||||
"home": {
|
||||
"path": "~",
|
||||
"description": "Home directory",
|
||||
"priority": 1
|
||||
},
|
||||
"docs": {
|
||||
"path": "~/Documents",
|
||||
"description": "Documents folder",
|
||||
"priority": 2
|
||||
},
|
||||
"downloads": {
|
||||
"path": "~/Downloads",
|
||||
"description": "Downloads folder",
|
||||
"priority": 3
|
||||
}
|
||||
},
|
||||
"default_path": "~",
|
||||
"editor": "vim",
|
||||
"file_manager": null
|
||||
}
|
||||
```
|
||||
|
||||
### 配置字段说明
|
||||
|
||||
- `version`: 配置文件版本号
|
||||
- `entries`: 跳转条目映射表
|
||||
- `path`: 目标路径(支持 `~` 和环境变量)
|
||||
- `description`: 可选描述信息
|
||||
- `priority`: 优先级(数字越小越优先)
|
||||
- `default_path`: 默认打开的路径
|
||||
- `editor`: 首选编辑器命令
|
||||
- `file_manager`: 首选文件管理器命令
|
||||
|
||||
## 命令速查表
|
||||
|
||||
### 主命令
|
||||
|
||||
| 命令 | 简写 | 说明 |
|
||||
|------|------|------|
|
||||
| `quickjump jump <name>` | `quickjump j <name>` | 跳转到指定目录 |
|
||||
| `quickjump jump --interactive` | `quickjump j -i` | 交互式选择跳转 |
|
||||
| `quickjump quick open <name>` | `quickjump k open <name>` | 快速打开目录 |
|
||||
| `quickjump quick --path <path>` | `quickjump k -p <path>` | 打开指定路径 |
|
||||
| `quickjump quick default` | `quickjump k -d` | 打开默认目录 |
|
||||
| `quickjump quick list` | `quickjump k -l` | 列出快速目标 |
|
||||
| `quickjump config <action>` | `quickjump c <action>` | 配置管理 |
|
||||
| `quickjump shell-integration` | - | 输出 Shell 集成脚本 |
|
||||
|
||||
### 全局选项
|
||||
|
||||
| 选项 | 简写 | 说明 |
|
||||
|------|------|------|
|
||||
| `--version` | `-v` | 显示版本信息 |
|
||||
| `--help` | `-h` | 显示帮助信息 |
|
||||
| `--quiet` | `-q` | 静默模式(抑制输出) |
|
||||
|
||||
### 配置子命令
|
||||
|
||||
| 命令 | 简写 | 说明 |
|
||||
|------|------|------|
|
||||
| `quickjump config add <name> <path>` | - | 添加条目 |
|
||||
| `quickjump config remove <name>` | `quickjump config rm <name>` | 删除条目 |
|
||||
| `quickjump config list` | `quickjump config ls` | 列出所有条目 |
|
||||
| `quickjump config set-default <path>` | - | 设置默认路径 |
|
||||
| `quickjump config set-editor <cmd>` | - | 设置编辑器 |
|
||||
| `quickjump config set-file-manager <cmd>` | - | 设置文件管理器 |
|
||||
| `quickjump config export <file>` | - | 导出配置 |
|
||||
| `quickjump config import <file>` | - | 导入配置 |
|
||||
| `quickjump config import <file> --merge` | `quickjump config import <file> -m` | 合并导入配置 |
|
||||
| `quickjump config edit` | - | 编辑配置文件 |
|
||||
| `quickjump config show` | - | 显示当前配置 |
|
||||
|
||||
### Shell 集成函数
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `qj <name>` | 跳转到指定目录 |
|
||||
| `qjq <name>` | 静默模式跳转 |
|
||||
| `qo <name>` | 快速打开目录 |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
quickjump/
|
||||
├── quickjump.cabal # Cabal 配置文件
|
||||
├── CHANGELOG.md # 版本变更记录
|
||||
├── README.md # 说明文档
|
||||
└── app/
|
||||
├── Main.hs # 程序入口和 CLI 解析
|
||||
├── Types.hs # 数据类型定义
|
||||
├── Config.hs # 配置管理
|
||||
├── Commands.hs # 命令实现
|
||||
└── Utils.hs # 工具函数
|
||||
```
|
||||
|
||||
## 依赖
|
||||
|
||||
- base >= 4.18
|
||||
- aeson >= 2.2
|
||||
- aeson-pretty >= 0.8
|
||||
- optparse-applicative >= 0.18
|
||||
- directory >= 1.3
|
||||
- filepath >= 1.4
|
||||
- process >= 1.6
|
||||
- text >= 2.0
|
||||
- bytestring >= 0.11
|
||||
- containers >= 0.6
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 更新日志
|
||||
|
||||
查看 [CHANGELOG.md](CHANGELOG.md) 了解详细的版本更新历史。
|
||||
395
app/Commands.hs
Normal file
395
app/Commands.hs
Normal file
@@ -0,0 +1,395 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Commands
|
||||
( runCommand
|
||||
, runJump
|
||||
, runQuick
|
||||
, runConfigCmd
|
||||
, printShellIntegration
|
||||
) where
|
||||
|
||||
import Control.Monad (forM_, unless, when)
|
||||
import Data.List (intercalate)
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Maybe (fromMaybe, isJust)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import System.Directory (doesDirectoryExist, doesFileExist)
|
||||
import System.Environment (lookupEnv)
|
||||
import System.Exit (exitFailure, exitSuccess)
|
||||
import System.FilePath ((</>))
|
||||
import System.Info (os)
|
||||
import System.IO (hFlush, stdout)
|
||||
import System.Process (callCommand, spawnCommand, waitForProcess)
|
||||
|
||||
import Config
|
||||
import Types
|
||||
import Utils
|
||||
|
||||
-- | 运行主命令
|
||||
runCommand :: Command -> IO ()
|
||||
runCommand cmd = case cmd of
|
||||
Jump name quiet -> runJump name quiet
|
||||
JumpInteractive quiet -> runJumpInteractive quiet
|
||||
Quick action quiet -> runQuick action quiet
|
||||
ConfigCmd action quiet -> runConfigCmd action quiet
|
||||
ShellIntegration -> printShellIntegration
|
||||
Version -> putStrLn "quickjump version 0.1.0.0"
|
||||
|
||||
-- | 跳转到指定条目
|
||||
runJump :: Text -> Bool -> IO ()
|
||||
runJump name quiet = do
|
||||
cfg <- ensureConfigExists
|
||||
case findEntry name cfg of
|
||||
Nothing -> do
|
||||
unless quiet $ do
|
||||
putStrLn $ "Unknown jump target: " ++ T.unpack name
|
||||
putStrLn "Use 'quickjump config list' to see available targets"
|
||||
exitFailure
|
||||
Just entry -> do
|
||||
expanded <- expandPath (path entry)
|
||||
exists <- doesDirectoryExist expanded
|
||||
if exists
|
||||
then do
|
||||
-- 输出 cd 命令供 shell 执行
|
||||
-- 使用引号包裹路径以处理包含空格的路径
|
||||
let cdCmd = case os of
|
||||
"mingw32" -> "cd \"" ++ expanded ++ "\""
|
||||
"mingw64" -> "cd \"" ++ expanded ++ "\""
|
||||
_ -> "cd " ++ show expanded
|
||||
putStrLn cdCmd
|
||||
else do
|
||||
unless quiet $ putStrLn $ "Directory does not exist: " ++ expanded
|
||||
exitFailure
|
||||
|
||||
-- | 交互式选择跳转
|
||||
runJumpInteractive :: Bool -> IO ()
|
||||
runJumpInteractive quiet = do
|
||||
cfg <- ensureConfigExists
|
||||
let sorted = getSortedEntries cfg
|
||||
if null sorted
|
||||
then unless quiet $ do
|
||||
putStrLn "No jump targets configured"
|
||||
putStrLn "Use 'quickjump config add <name> <path>' to add one"
|
||||
else do
|
||||
unless quiet $ do
|
||||
putStrLn "Available jump targets:"
|
||||
putStrLn ""
|
||||
forM_ (zip [1..] sorted) $ \(i, (name, entry)) -> do
|
||||
let desc = fromMaybe "" (description entry)
|
||||
putStrLn $ " " ++ show (i :: Int) ++ ". " ++ T.unpack name
|
||||
++ " -> " ++ path entry
|
||||
++ if T.null desc then "" else " (" ++ T.unpack desc ++ ")"
|
||||
putStr "\nSelect target (number or name): "
|
||||
hFlush stdout
|
||||
selection <- getLine
|
||||
case reads selection of
|
||||
[(n, "")] | n > 0 && n <= length sorted ->
|
||||
runJump (fst $ sorted !! (n - 1)) quiet
|
||||
_ -> runJump (T.pack selection) quiet
|
||||
|
||||
-- | 运行快速操作
|
||||
runQuick :: QuickAction -> Bool -> IO ()
|
||||
runQuick action quiet = do
|
||||
cfg <- ensureConfigExists
|
||||
case action of
|
||||
QuickOpen name -> do
|
||||
case findEntry name cfg of
|
||||
Nothing -> do
|
||||
unless quiet $ putStrLn $ "Unknown quick target: " ++ T.unpack name
|
||||
exitFailure
|
||||
Just entry -> openPath (path entry) cfg quiet
|
||||
|
||||
QuickOpenPath p -> openPath p cfg quiet
|
||||
|
||||
QuickList -> do
|
||||
let sorted = getSortedEntries cfg
|
||||
unless quiet $ do
|
||||
putStrLn "Quick access targets:"
|
||||
putStrLn ""
|
||||
forM_ sorted $ \(name, entry) -> do
|
||||
let desc = fromMaybe "" (description entry)
|
||||
putStrLn $ " " ++ padRight 15 (T.unpack name)
|
||||
++ " -> " ++ padRight 30 (path entry)
|
||||
++ if T.null desc then "" else " # " ++ T.unpack desc
|
||||
|
||||
QuickDefault ->
|
||||
case defaultPath cfg of
|
||||
Nothing -> do
|
||||
unless quiet $ do
|
||||
putStrLn "No default path configured"
|
||||
putStrLn "Use 'quickjump config set-default <path>' to set one"
|
||||
exitFailure
|
||||
Just p -> openPath p cfg quiet
|
||||
|
||||
-- | 打开路径(使用文件管理器或 cd)
|
||||
openPath :: FilePath -> Config -> Bool -> IO ()
|
||||
openPath p cfg quiet = do
|
||||
expanded <- expandPath p
|
||||
exists <- doesDirectoryExist expanded
|
||||
unless exists $ do
|
||||
unless quiet $ putStrLn $ "Directory does not exist: " ++ expanded
|
||||
exitFailure
|
||||
|
||||
-- 尝试使用配置的文件管理器,或者自动检测
|
||||
let fm = fileManager cfg
|
||||
case fm of
|
||||
Just cmd -> runFileManager cmd expanded quiet
|
||||
Nothing -> autoDetectAndOpen expanded quiet
|
||||
|
||||
-- | 自动检测并打开文件管理器
|
||||
autoDetectAndOpen :: FilePath -> Bool -> IO ()
|
||||
autoDetectAndOpen path quiet = do
|
||||
let (cmd, args) = case os of
|
||||
"darwin" -> ("open", [path])
|
||||
"mingw32" -> ("explorer", [path])
|
||||
"mingw64" -> ("explorer", [path])
|
||||
"cygwin" -> ("cygstart", [path])
|
||||
_ -> ("xdg-open", [path]) -- Linux and others
|
||||
|
||||
-- 检查命令是否存在
|
||||
exists <- commandExists cmd
|
||||
if exists
|
||||
then do
|
||||
_ <- spawnCommand (unwords (cmd : map show args)) >>= waitForProcess
|
||||
return ()
|
||||
else do
|
||||
unless quiet $ do
|
||||
putStrLn $ "Cannot open file manager. Please configure one:"
|
||||
putStrLn $ " quickjump config set-file-manager <command>"
|
||||
-- 输出 cd 命令作为备选
|
||||
putStrLn $ "cd " ++ show path
|
||||
|
||||
-- | 运行文件管理器
|
||||
runFileManager :: FilePath -> FilePath -> Bool -> IO ()
|
||||
runFileManager cmd path quiet = do
|
||||
expanded <- expandPath path
|
||||
let fullCmd = cmd ++ " " ++ show expanded
|
||||
_ <- spawnCommand fullCmd >>= waitForProcess
|
||||
return ()
|
||||
|
||||
-- | 运行配置命令
|
||||
runConfigCmd :: ConfigAction -> Bool -> IO ()
|
||||
runConfigCmd action quiet = do
|
||||
cfg <- ensureConfigExists
|
||||
case action of
|
||||
ConfigAdd name path mDesc -> do
|
||||
expanded <- expandPath path
|
||||
exists <- doesDirectoryExist expanded
|
||||
unless exists $ do
|
||||
unless quiet $ putStrLn $ "Warning: Directory does not exist: " ++ expanded
|
||||
let entry = JumpEntry
|
||||
{ path = path
|
||||
, description = mDesc
|
||||
, priority = 100
|
||||
}
|
||||
newCfg = cfg { entries = M.insert name entry (entries cfg) }
|
||||
saveConfig newCfg
|
||||
unless quiet $ putStrLn $ "Added '" ++ T.unpack name ++ "' -> " ++ path
|
||||
|
||||
ConfigRemove name -> do
|
||||
if M.member name (entries cfg)
|
||||
then do
|
||||
let newCfg = cfg { entries = M.delete name (entries cfg) }
|
||||
saveConfig newCfg
|
||||
unless quiet $ putStrLn $ "Removed '" ++ T.unpack name ++ "'"
|
||||
else do
|
||||
unless quiet $ putStrLn $ "No such entry: '" ++ T.unpack name ++ "'"
|
||||
exitFailure
|
||||
|
||||
ConfigList -> do
|
||||
let sorted = getSortedEntries cfg
|
||||
unless quiet $ do
|
||||
if null sorted
|
||||
then putStrLn "No entries configured"
|
||||
else do
|
||||
putStrLn "Configured jump entries:"
|
||||
putStrLn ""
|
||||
forM_ sorted $ \(name, entry) -> do
|
||||
let desc = fromMaybe "" (description entry)
|
||||
putStrLn $ " " ++ padRight 15 (T.unpack name)
|
||||
++ " -> " ++ padRight 30 (path entry)
|
||||
++ if T.null desc then "" else " # " ++ T.unpack desc
|
||||
|
||||
ConfigSetDefault path -> do
|
||||
expanded <- expandPath path
|
||||
exists <- doesDirectoryExist expanded
|
||||
unless exists $ do
|
||||
unless quiet $ putStrLn $ "Warning: Directory does not exist: " ++ expanded
|
||||
let newCfg = cfg { defaultPath = Just path }
|
||||
saveConfig newCfg
|
||||
unless quiet $ putStrLn $ "Set default path to: " ++ path
|
||||
|
||||
ConfigSetEditor cmd -> do
|
||||
let newCfg = cfg { editor = Just cmd }
|
||||
saveConfig newCfg
|
||||
unless quiet $ putStrLn $ "Set editor to: " ++ cmd
|
||||
|
||||
ConfigSetFileManager cmd -> do
|
||||
let newCfg = cfg { fileManager = Just cmd }
|
||||
saveConfig newCfg
|
||||
unless quiet $ putStrLn $ "Set file manager to: " ++ cmd
|
||||
|
||||
ConfigExport path -> do
|
||||
saveConfigTo path cfg
|
||||
unless quiet $ putStrLn $ "Config exported to: " ++ path
|
||||
|
||||
ConfigImport path merge -> do
|
||||
imported <- loadConfigFrom path
|
||||
let merged = mergeConfigs cfg imported merge
|
||||
saveConfig merged
|
||||
unless quiet $
|
||||
if merge
|
||||
then putStrLn "Config imported and merged successfully"
|
||||
else putStrLn "Config imported (replaced existing)"
|
||||
|
||||
ConfigEdit -> do
|
||||
let ed = fromMaybe (defaultEditor os) (editor cfg)
|
||||
configPath <- getConfigPath
|
||||
_ <- spawnCommand (ed ++ " " ++ show configPath) >>= waitForProcess
|
||||
return ()
|
||||
|
||||
ConfigShow -> do
|
||||
configPath <- getConfigPath
|
||||
unless quiet $ do
|
||||
putStrLn $ "Config location: " ++ configPath
|
||||
putStrLn $ "Version: " ++ T.unpack (version cfg)
|
||||
putStrLn $ "Entries: " ++ show (M.size $ entries cfg)
|
||||
putStrLn $ "Default path: " ++ fromMaybe "(not set)" (defaultPath cfg)
|
||||
putStrLn $ "Editor: " ++ fromMaybe "(not set)" (editor cfg)
|
||||
putStrLn $ "File manager: " ++ fromMaybe "(auto-detect)" (fileManager cfg)
|
||||
|
||||
-- | 获取默认编辑器
|
||||
defaultEditor :: String -> String
|
||||
defaultEditor platform = case platform of
|
||||
"darwin" -> "open -t"
|
||||
"mingw32" -> "notepad"
|
||||
"mingw64" -> "notepad"
|
||||
_ -> "vi"
|
||||
|
||||
-- | 打印 shell 集成脚本
|
||||
printShellIntegration :: IO ()
|
||||
printShellIntegration = do
|
||||
putStrLn $ shellScript os
|
||||
|
||||
-- | 获取对应 shell 的集成脚本
|
||||
shellScript :: String -> String
|
||||
shellScript platform = case platform of
|
||||
"mingw32" -> windowsPowerShellScript
|
||||
"mingw64" -> windowsPowerShellScript
|
||||
"cygwin" -> bashScript
|
||||
"darwin" -> bashScript
|
||||
_ -> bashScript
|
||||
|
||||
-- | Bash/Zsh 集成脚本
|
||||
bashScript :: String
|
||||
bashScript = intercalate "\n"
|
||||
[ "# QuickJump Shell Integration for Bash/Zsh"
|
||||
, "# Add this to your shell profile (.bashrc, .zshrc, etc.)"
|
||||
, "#"
|
||||
, "# eval \"$(quickjump shell-integration)\""
|
||||
, ""
|
||||
, "# Bash/Zsh function for directory jumping"
|
||||
, "qj() {"
|
||||
, " local output=$(quickjump jump \"$1\")"
|
||||
, " if [[ $output == cd* ]]; then"
|
||||
, " eval \"$output\""
|
||||
, " else"
|
||||
, " echo \"$output\""
|
||||
, " fi"
|
||||
, "}"
|
||||
, ""
|
||||
, "# Quick open function"
|
||||
, "qo() {"
|
||||
, " quickjump quick \"$1\""
|
||||
, "}"
|
||||
, ""
|
||||
, "# Quiet mode function"
|
||||
, "qjq() {"
|
||||
, " quickjump --quiet jump \"$1\""
|
||||
, "}"
|
||||
, ""
|
||||
, "# Tab completion for bash"
|
||||
, "if [ -n \"$BASH_VERSION\" ]; then"
|
||||
, " _qj_complete() {"
|
||||
, " local cur=\"${COMP_WORDS[COMP_CWORD]}\""
|
||||
, " local entries=$(quickjump config list 2>/dev/null | grep '^ ' | awk '{print $1}')"
|
||||
, " COMPREPLY=($(compgen -W \"$entries\" -- \"$cur\"))"
|
||||
, " }"
|
||||
, " complete -F _qj_complete qj"
|
||||
, " complete -F _qj_complete qjq"
|
||||
, "fi"
|
||||
, ""
|
||||
, "# Tab completion for zsh"
|
||||
, "if [ -n \"$ZSH_VERSION\" ]; then"
|
||||
, " _qj_complete() {"
|
||||
, " local -a entries"
|
||||
, " entries=(${(f)\"$(quickjump config list 2>/dev/null | grep '^ ' | awk '{print $1}')\"})"
|
||||
, " _describe 'jump targets' entries"
|
||||
, " }"
|
||||
, " compdef _qj_complete qj"
|
||||
, " compdef _qj_complete qjq"
|
||||
, "fi"
|
||||
]
|
||||
|
||||
-- | Windows PowerShell 集成脚本
|
||||
windowsPowerShellScript :: String
|
||||
windowsPowerShellScript = intercalate "\n"
|
||||
[ "# QuickJump Shell Integration for PowerShell"
|
||||
, "# Add this to your PowerShell profile ($PROFILE)"
|
||||
, "#"
|
||||
, "# To edit your profile: notepad $PROFILE"
|
||||
, "#"
|
||||
, "# . (quickjump shell-integration | Out-String)"
|
||||
, ""
|
||||
, "# Function for directory jumping"
|
||||
, "function qj {"
|
||||
, " param([string]$name)"
|
||||
, " $output = quickjump jump $name"
|
||||
, " if ($output -like 'cd *') {"
|
||||
, " # Remove 'cd ' prefix and execute"
|
||||
, " $path = $output -replace '^cd \"?([^\"\"]*)\"?$', '$1'"
|
||||
, " Set-Location $path"
|
||||
, " } else {"
|
||||
, " Write-Output $output"
|
||||
, " }"
|
||||
, "}"
|
||||
, ""
|
||||
, "# Quiet mode function"
|
||||
, "function qjq {"
|
||||
, " param([string]$name)"
|
||||
, " $output = quickjump --quiet jump $name"
|
||||
, " if ($output -like 'cd *') {"
|
||||
, " $path = $output -replace '^cd \"?([^\"\"]*)\"?$', '$1'"
|
||||
, " Set-Location $path"
|
||||
, " }"
|
||||
, "}"
|
||||
, ""
|
||||
, "# Quick open function"
|
||||
, "function qo {"
|
||||
, " param([string]$name)"
|
||||
, " quickjump quick $name"
|
||||
, "}"
|
||||
, ""
|
||||
, "# Tab completion"
|
||||
, "Register-ArgumentCompleter -CommandName qj -ScriptBlock {"
|
||||
, " param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)"
|
||||
, " $entries = quickjump config list 2>$null | Select-String '^ ' | ForEach-Object {"
|
||||
, " $_.ToString().Trim().Split()[0]"
|
||||
, " }"
|
||||
, " $entries | Where-Object { $_ -like \"$wordToComplete*\" } | ForEach-Object {"
|
||||
, " [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)"
|
||||
, " }"
|
||||
, "}"
|
||||
, ""
|
||||
, "Register-ArgumentCompleter -CommandName qjq -ScriptBlock {"
|
||||
, " param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)"
|
||||
, " $entries = quickjump config list 2>$null | Select-String '^ ' | ForEach-Object {"
|
||||
, " $_.ToString().Trim().Split()[0]"
|
||||
, " }"
|
||||
, " $entries | Where-Object { $_ -like \"$wordToComplete*\" } | ForEach-Object {"
|
||||
, " [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)"
|
||||
, " }"
|
||||
, "}"
|
||||
]
|
||||
168
app/Config.hs
Normal file
168
app/Config.hs
Normal file
@@ -0,0 +1,168 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Config
|
||||
( getConfigPath
|
||||
, loadConfig
|
||||
, loadConfigFrom
|
||||
, saveConfig
|
||||
, saveConfigTo
|
||||
, ensureConfigExists
|
||||
, findEntry
|
||||
, expandPath
|
||||
, getSortedEntries
|
||||
, mergeConfigs
|
||||
) where
|
||||
|
||||
import Control.Exception (catch, throwIO)
|
||||
import Control.Monad (unless, when)
|
||||
import Data.Aeson (eitherDecode, encode)
|
||||
import Data.Aeson.Encode.Pretty (encodePretty)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.List (sortOn)
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist,
|
||||
getHomeDirectory)
|
||||
import System.Environment (lookupEnv)
|
||||
import System.FilePath ((</>), takeDirectory)
|
||||
import System.IO.Error (isDoesNotExistError)
|
||||
|
||||
import Types
|
||||
|
||||
-- | 获取配置文件路径
|
||||
getConfigPath :: IO FilePath
|
||||
getConfigPath = do
|
||||
-- 首先检查环境变量
|
||||
mEnvPath <- lookupEnv "QUICKJUMP_CONFIG"
|
||||
case mEnvPath of
|
||||
Just path -> return path
|
||||
Nothing -> do
|
||||
-- 默认使用 XDG 配置目录
|
||||
xdgConfig <- lookupEnv "XDG_CONFIG_HOME"
|
||||
configDir <- case xdgConfig of
|
||||
Just dir -> return dir
|
||||
Nothing -> do
|
||||
home <- getHomeDirectory
|
||||
return $ home </> ".config"
|
||||
return $ configDir </> "quickjump" </> "config.json"
|
||||
|
||||
-- | 展开路径中的 ~ 和环境变量
|
||||
expandPath :: FilePath -> IO FilePath
|
||||
expandPath path = do
|
||||
-- 首先处理 ~ 展开
|
||||
expanded1 <- if take 1 path == "~"
|
||||
then do
|
||||
home <- getHomeDirectory
|
||||
return $ home </> drop 2 path
|
||||
else return path
|
||||
-- 然后处理环境变量(支持 Unix $VAR 和 Windows %VAR% 格式)
|
||||
expandEnvVars expanded1
|
||||
|
||||
-- | 展开环境变量
|
||||
expandEnvVars :: FilePath -> IO FilePath
|
||||
expandEnvVars path = do
|
||||
-- 处理 Unix 风格的环境变量 $VAR
|
||||
let expandUnixVars s = case s of
|
||||
'$':'{':rest ->
|
||||
case break (=='}') rest of
|
||||
(var, '}':remaining) -> do
|
||||
mval <- lookupEnv var
|
||||
case mval of
|
||||
Just val -> (val ++) <$> expandEnvVars remaining
|
||||
Nothing -> (("${" ++ var ++ "}") ++) <$> expandEnvVars remaining
|
||||
_ -> ('$':) <$> expandEnvVars rest
|
||||
'$':rest ->
|
||||
case span (\c -> isAlphaNum c || c == '_') rest of
|
||||
(var, remaining) -> do
|
||||
mval <- lookupEnv var
|
||||
case mval of
|
||||
Just val -> (val ++) <$> expandEnvVars remaining
|
||||
Nothing -> (('$' : var) ++) <$> expandEnvVars remaining
|
||||
c:cs -> (c:) <$> expandEnvVars cs
|
||||
[] -> return []
|
||||
-- 处理 Windows 风格的环境变量 %VAR%
|
||||
let expandWindowsVars s = case s of
|
||||
'%':rest ->
|
||||
case break (=='%') rest of
|
||||
(var, '%':remaining) -> do
|
||||
mval <- lookupEnv var
|
||||
case mval of
|
||||
Just val -> (val ++) <$> expandEnvVars remaining
|
||||
Nothing -> (('%' : var ++ "%") ++) <$> expandEnvVars remaining
|
||||
_ -> ('%':) <$> expandEnvVars rest
|
||||
c:cs -> (c:) <$> expandEnvVars cs
|
||||
[] -> return []
|
||||
-- 根据操作系统选择展开方式
|
||||
if '%' `elem` path
|
||||
then expandWindowsVars path
|
||||
else expandUnixVars path
|
||||
where
|
||||
isAlphaNum c = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
||||
|
||||
-- | 确保配置文件存在(如果不存在则创建默认配置)
|
||||
ensureConfigExists :: IO Config
|
||||
ensureConfigExists = do
|
||||
configPath <- getConfigPath
|
||||
exists <- doesFileExist configPath
|
||||
if exists
|
||||
then loadConfig
|
||||
else do
|
||||
putStrLn $ "Config not found, creating default at: " ++ configPath
|
||||
createDirectoryIfMissing True (takeDirectory configPath)
|
||||
saveConfig defaultConfig
|
||||
return defaultConfig
|
||||
|
||||
-- | 加载配置
|
||||
loadConfig :: IO Config
|
||||
loadConfig = do
|
||||
configPath <- getConfigPath
|
||||
loadConfigFrom configPath
|
||||
|
||||
-- | 从指定路径加载配置
|
||||
loadConfigFrom :: FilePath -> IO Config
|
||||
loadConfigFrom path = do
|
||||
expanded <- expandPath path
|
||||
result <- catch
|
||||
(Right <$> BL.readFile expanded)
|
||||
(\e -> if isDoesNotExistError e
|
||||
then return $ Left $ "Config file not found: " ++ expanded
|
||||
else throwIO e)
|
||||
case result of
|
||||
Left err -> error err
|
||||
Right bs ->
|
||||
case eitherDecode bs of
|
||||
Left err -> error $ "Failed to parse config: " ++ err
|
||||
Right cfg -> return cfg
|
||||
|
||||
-- | 保存配置
|
||||
saveConfig :: Config -> IO ()
|
||||
saveConfig cfg = do
|
||||
configPath <- getConfigPath
|
||||
saveConfigTo configPath cfg
|
||||
|
||||
-- | 保存配置到指定路径
|
||||
saveConfigTo :: FilePath -> Config -> IO ()
|
||||
saveConfigTo path cfg = do
|
||||
expanded <- expandPath path
|
||||
createDirectoryIfMissing True (takeDirectory expanded)
|
||||
BL.writeFile expanded (encodePretty cfg)
|
||||
|
||||
-- | 查找条目
|
||||
findEntry :: Text -> Config -> Maybe JumpEntry
|
||||
findEntry name cfg = M.lookup name (entries cfg)
|
||||
|
||||
-- | 获取按优先级排序的条目列表
|
||||
getSortedEntries :: Config -> [(Text, JumpEntry)]
|
||||
getSortedEntries cfg =
|
||||
sortOn (priority . snd) $ M.toList (entries cfg)
|
||||
|
||||
-- | 合并两个配置(用于导入)
|
||||
mergeConfigs :: Config -> Config -> Bool -> Config
|
||||
mergeConfigs base new shouldMerge =
|
||||
if shouldMerge
|
||||
then base { entries = M.union (entries new) (entries base) }
|
||||
else new
|
||||
190
app/Main.hs
Normal file
190
app/Main.hs
Normal file
@@ -0,0 +1,190 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Main where
|
||||
|
||||
-- import Data.Text (Text)
|
||||
-- import qualified Data.Text as T
|
||||
import Options.Applicative
|
||||
import System.Environment (getArgs, withArgs)
|
||||
-- import System.IO (hPutStrLn, stderr)
|
||||
|
||||
import Commands
|
||||
import Types
|
||||
|
||||
-- | 主函数
|
||||
main :: IO ()
|
||||
main = do
|
||||
args <- getArgs
|
||||
-- 如果没有参数,显示帮助
|
||||
if null args
|
||||
then withArgs ["--help"] runParser
|
||||
else runParser
|
||||
where
|
||||
runParser = do
|
||||
cmd <- execParser opts
|
||||
runCommand cmd
|
||||
|
||||
opts = info (helper <*> versionOption <*> commandParser)
|
||||
( fullDesc
|
||||
<> progDesc "QuickJump - Fast directory navigation tool"
|
||||
<> header "quickjump - A command line tool for quick directory jumping" )
|
||||
|
||||
-- | 静默模式选项
|
||||
quietOption :: Parser Bool
|
||||
quietOption = switch
|
||||
( long "quiet"
|
||||
<> short 'q'
|
||||
<> help "Suppress output messages (quiet mode)" )
|
||||
|
||||
-- | 版本选项
|
||||
versionOption :: Parser (a -> a)
|
||||
versionOption = infoOption "quickjump 0.3.0.1"
|
||||
( long "version"
|
||||
<> short 'v'
|
||||
<> help "Show version information" )
|
||||
|
||||
-- | 主命令解析器
|
||||
commandParser :: Parser Command
|
||||
commandParser = subparser
|
||||
( command "jump" (info jumpParser
|
||||
( progDesc "Jump to a configured directory" ))
|
||||
<> command "j" (info jumpParser
|
||||
( progDesc "Alias for jump" ))
|
||||
<> command "quick" (info quickParser
|
||||
( progDesc "Quick open a directory" ))
|
||||
<> command "k" (info quickParser
|
||||
( progDesc "Alias for quick" ))
|
||||
<> command "config" (info configParser
|
||||
( progDesc "Manage configuration" ))
|
||||
<> command "c" (info configParser
|
||||
( progDesc "Alias for config" ))
|
||||
<> command "shell-integration" (info shellIntegrationParser
|
||||
( progDesc "Output shell integration script" ))
|
||||
)
|
||||
<|> jumpParser -- 默认命令是 jump
|
||||
|
||||
-- | 跳转命令解析器
|
||||
jumpParser :: Parser Command
|
||||
jumpParser = (Jump <$> argument str
|
||||
( metavar "NAME"
|
||||
<> help "Name of the jump target" ))
|
||||
<*> quietOption
|
||||
<|> (JumpInteractive <$> flag' False
|
||||
( long "interactive"
|
||||
<> short 'i'
|
||||
<> help "Interactive mode - select from list" ))
|
||||
|
||||
-- | 快速命令解析器
|
||||
quickParser :: Parser Command
|
||||
quickParser = (Quick <$> subparser
|
||||
( command "open" (info (QuickOpen <$> argument str (metavar "NAME"))
|
||||
( progDesc "Open a configured directory" ))
|
||||
<> command "list" (info (pure QuickList)
|
||||
( progDesc "List all quick access targets" ))
|
||||
<> command "default" (info (pure QuickDefault)
|
||||
( progDesc "Open the default directory" ))
|
||||
)
|
||||
<*> quietOption)
|
||||
<|> (Quick <$> (QuickOpen <$> strOption
|
||||
( long "open"
|
||||
<> short 'o'
|
||||
<> metavar "NAME"
|
||||
<> help "Open the specified target" ))
|
||||
<*> quietOption)
|
||||
<|> (Quick <$> (QuickOpenPath <$> strOption
|
||||
( long "path"
|
||||
<> short 'p'
|
||||
<> metavar "PATH"
|
||||
<> help "Open the specified path" ))
|
||||
<*> quietOption)
|
||||
<|> (Quick <$> flag' QuickList
|
||||
( long "list"
|
||||
<> short 'l'
|
||||
<> help "List all targets" )
|
||||
<*> quietOption)
|
||||
<|> (Quick <$> flag' QuickDefault
|
||||
( long "default"
|
||||
<> short 'd'
|
||||
<> help "Open default directory" )
|
||||
<*> quietOption)
|
||||
<|> (Quick <$> (QuickOpen <$> argument str (metavar "NAME" <> help "Target name or path"))
|
||||
<*> quietOption)
|
||||
|
||||
-- | 配置命令解析器
|
||||
configParser :: Parser Command
|
||||
configParser = ConfigCmd <$> subparser
|
||||
( command "add" (info addParser
|
||||
( progDesc "Add a new jump entry" ))
|
||||
<> command "remove" (info removeParser
|
||||
( progDesc "Remove a jump entry" ))
|
||||
<> command "rm" (info removeParser
|
||||
( progDesc "Alias for remove" ))
|
||||
<> command "list" (info (pure ConfigList)
|
||||
( progDesc "List all entries" ))
|
||||
<> command "ls" (info (pure ConfigList)
|
||||
( progDesc "Alias for list" ))
|
||||
<> command "set-default" (info setDefaultParser
|
||||
( progDesc "Set the default path" ))
|
||||
<> command "set-editor" (info setEditorParser
|
||||
( progDesc "Set the preferred editor" ))
|
||||
<> command "set-file-manager" (info setFileManagerParser
|
||||
( progDesc "Set the preferred file manager" ))
|
||||
<> command "export" (info exportParser
|
||||
( progDesc "Export configuration to file" ))
|
||||
<> command "import" (info importParser
|
||||
( progDesc "Import configuration from file" ))
|
||||
<> command "edit" (info (pure ConfigEdit)
|
||||
( progDesc "Edit configuration with editor" ))
|
||||
<> command "show" (info (pure ConfigShow)
|
||||
( progDesc "Show current configuration" ))
|
||||
)
|
||||
<*> quietOption
|
||||
|
||||
-- | 添加条目解析器
|
||||
addParser :: Parser ConfigAction
|
||||
addParser = ConfigAdd
|
||||
<$> argument str (metavar "NAME" <> help "Entry name")
|
||||
<*> argument str (metavar "PATH" <> help "Directory path")
|
||||
<*> optional (strOption
|
||||
( long "description"
|
||||
<> short 'd'
|
||||
<> metavar "DESC"
|
||||
<> help "Optional description" ))
|
||||
|
||||
-- | 删除条目解析器
|
||||
removeParser :: Parser ConfigAction
|
||||
removeParser = ConfigRemove
|
||||
<$> argument str (metavar "NAME" <> help "Entry name to remove")
|
||||
|
||||
-- | 设置默认路径解析器
|
||||
setDefaultParser :: Parser ConfigAction
|
||||
setDefaultParser = ConfigSetDefault
|
||||
<$> argument str (metavar "PATH" <> help "Default directory path")
|
||||
|
||||
-- | 设置编辑器解析器
|
||||
setEditorParser :: Parser ConfigAction
|
||||
setEditorParser = ConfigSetEditor
|
||||
<$> argument str (metavar "COMMAND" <> help "Editor command")
|
||||
|
||||
-- | 设置文件管理器解析器
|
||||
setFileManagerParser :: Parser ConfigAction
|
||||
setFileManagerParser = ConfigSetFileManager
|
||||
<$> argument str (metavar "COMMAND" <> help "File manager command")
|
||||
|
||||
-- | 导出配置解析器
|
||||
exportParser :: Parser ConfigAction
|
||||
exportParser = ConfigExport
|
||||
<$> argument str (metavar "FILE" <> help "Export file path")
|
||||
|
||||
-- | 导入配置解析器
|
||||
importParser :: Parser ConfigAction
|
||||
importParser = ConfigImport
|
||||
<$> argument str (metavar "FILE" <> help "Import file path")
|
||||
<*> switch
|
||||
( long "merge"
|
||||
<> short 'm'
|
||||
<> help "Merge with existing config instead of replacing" )
|
||||
|
||||
-- | Shell 集成解析器
|
||||
shellIntegrationParser :: Parser Command
|
||||
shellIntegrationParser = pure ShellIntegration
|
||||
132
app/Types.hs
Normal file
132
app/Types.hs
Normal file
@@ -0,0 +1,132 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Types
|
||||
( Config(..)
|
||||
, JumpEntry(..)
|
||||
, Command(..)
|
||||
, QuickAction(..)
|
||||
, ConfigAction(..)
|
||||
, defaultConfig
|
||||
, emptyConfig
|
||||
) where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Text (Text)
|
||||
import GHC.Generics
|
||||
|
||||
-- | 单个跳转条目
|
||||
data JumpEntry = JumpEntry
|
||||
{ path :: FilePath -- ^ 目标路径
|
||||
, description :: Maybe Text -- ^ 可选描述
|
||||
, priority :: Int -- ^ 优先级(数字越小越优先)
|
||||
} deriving (Show, Eq, Generic)
|
||||
|
||||
instance ToJSON JumpEntry where
|
||||
toJSON entry = object
|
||||
[ "path" .= path entry
|
||||
, "description" .= description entry
|
||||
, "priority" .= priority entry
|
||||
]
|
||||
|
||||
instance FromJSON JumpEntry where
|
||||
parseJSON = withObject "JumpEntry" $ \v -> JumpEntry
|
||||
<$> v .: "path"
|
||||
<*> v .:? "description"
|
||||
<*> v .:? "priority" .!= 100
|
||||
|
||||
-- | 配置文件结构
|
||||
data Config = Config
|
||||
{ version :: Text -- ^ 配置版本
|
||||
, entries :: Map Text JumpEntry -- ^ 命名跳转条目
|
||||
, defaultPath :: Maybe FilePath -- ^ 默认打开路径
|
||||
, editor :: Maybe FilePath -- ^ 首选编辑器
|
||||
, fileManager :: Maybe FilePath -- ^ 首选文件管理器
|
||||
} deriving (Show, Eq, Generic)
|
||||
|
||||
instance ToJSON Config where
|
||||
toJSON cfg = object
|
||||
[ "version" .= version cfg
|
||||
, "entries" .= entries cfg
|
||||
, "default_path" .= defaultPath cfg
|
||||
, "editor" .= editor cfg
|
||||
, "file_manager" .= fileManager cfg
|
||||
]
|
||||
|
||||
instance FromJSON Config where
|
||||
parseJSON = withObject "Config" $ \v -> Config
|
||||
<$> v .:? "version" .!= "1.0"
|
||||
<*> v .:? "entries" .!= M.empty
|
||||
<*> v .:? "default_path"
|
||||
<*> v .:? "editor"
|
||||
<*> v .:? "file_manager"
|
||||
|
||||
-- | 快速操作类型
|
||||
data QuickAction
|
||||
= QuickOpen Text -- ^ 打开配置中的指定条目
|
||||
| QuickOpenPath FilePath -- ^ 打开指定路径
|
||||
| QuickList -- ^ 列出所有快速条目
|
||||
| QuickDefault -- ^ 打开默认路径
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 配置操作类型
|
||||
data ConfigAction
|
||||
= ConfigAdd Text FilePath (Maybe Text) -- ^ 添加条目: 名称 路径 [描述]
|
||||
| ConfigRemove Text -- ^ 删除条目
|
||||
| ConfigList -- ^ 列出所有条目
|
||||
| ConfigSetDefault FilePath -- ^ 设置默认路径
|
||||
| ConfigSetEditor FilePath -- ^ 设置编辑器
|
||||
| ConfigSetFileManager FilePath -- ^ 设置文件管理器
|
||||
| ConfigExport FilePath -- ^ 导出配置到文件
|
||||
| ConfigImport FilePath Bool -- ^ 导入配置 (文件路径, 是否合并)
|
||||
| ConfigEdit -- ^ 用编辑器打开配置文件
|
||||
| ConfigShow -- ^ 显示当前配置
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 主命令类型
|
||||
data Command
|
||||
= Jump Text Bool -- ^ 跳转到指定条目 (名称, 是否静默)
|
||||
| JumpInteractive Bool -- ^ 交互式选择跳转 (是否静默)
|
||||
| Quick QuickAction Bool -- ^ 快速操作 (操作, 是否静默)
|
||||
| ConfigCmd ConfigAction Bool -- ^ 配置操作 (操作, 是否静默)
|
||||
| ShellIntegration -- ^ 输出 shell 集成脚本
|
||||
| Version -- ^ 显示版本
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 空配置
|
||||
emptyConfig :: Config
|
||||
emptyConfig = Config
|
||||
{ version = "1.0"
|
||||
, entries = M.empty
|
||||
, defaultPath = Nothing
|
||||
, editor = Nothing
|
||||
, fileManager = Nothing
|
||||
}
|
||||
|
||||
-- | 默认配置(带示例)
|
||||
defaultConfig :: Config
|
||||
defaultConfig = Config
|
||||
{ version = "1.0"
|
||||
, entries = M.fromList
|
||||
[ ("home", JumpEntry
|
||||
{ path = "~"
|
||||
, description = Just "Home directory"
|
||||
, priority = 1
|
||||
})
|
||||
, ("docs", JumpEntry
|
||||
{ path = "~/Documents"
|
||||
, description = Just "Documents folder"
|
||||
, priority = 2
|
||||
})
|
||||
, ("downloads", JumpEntry
|
||||
{ path = "~/Downloads"
|
||||
, description = Just "Downloads folder"
|
||||
, priority = 3
|
||||
})
|
||||
]
|
||||
, defaultPath = Just "~"
|
||||
, editor = Just "vim"
|
||||
, fileManager = Nothing
|
||||
}
|
||||
88
app/Utils.hs
Normal file
88
app/Utils.hs
Normal file
@@ -0,0 +1,88 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Utils
|
||||
( padRight
|
||||
, commandExists
|
||||
, formatTable
|
||||
, truncatePath
|
||||
) where
|
||||
|
||||
import Control.Exception (catch)
|
||||
import Data.List (intercalate, transpose)
|
||||
import System.Directory (findExecutable)
|
||||
import System.IO.Error (isDoesNotExistError)
|
||||
|
||||
-- | 右填充字符串到指定长度
|
||||
padRight :: Int -> String -> String
|
||||
padRight n s = s ++ replicate (max 0 (n - length s)) ' '
|
||||
|
||||
-- | 左填充字符串到指定长度
|
||||
padLeft :: Int -> String -> String
|
||||
padLeft n s = replicate (max 0 (n - length s)) ' ' ++ s
|
||||
|
||||
-- | 检查命令是否存在
|
||||
commandExists :: String -> IO Bool
|
||||
commandExists cmd = do
|
||||
result <- findExecutable cmd
|
||||
return $ case result of
|
||||
Just _ -> True
|
||||
Nothing -> False
|
||||
|
||||
-- | 截断路径显示
|
||||
truncatePath :: Int -> String -> String
|
||||
truncatePath maxLen path
|
||||
| length path <= maxLen = path
|
||||
| otherwise = "..." ++ drop (length path - maxLen + 3) path
|
||||
|
||||
-- | 格式化表格
|
||||
data TableCell = TableCell String Int -- ^ 内容和对齐宽度
|
||||
|
||||
formatTable :: [[String]] -> String
|
||||
formatTable rows =
|
||||
let -- 计算每列的最大宽度
|
||||
colWidths = map maximum $ transpose
|
||||
[ map length row | row <- rows ]
|
||||
-- 格式化每一行
|
||||
formatRow row = intercalate " "
|
||||
[ padRight w cell | (cell, w) <- zip row colWidths ]
|
||||
in intercalate "\n" $ map formatRow rows
|
||||
|
||||
-- | 安全的读取文件
|
||||
safeReadFile :: FilePath -> IO (Maybe String)
|
||||
safeReadFile path = do
|
||||
result <- catch
|
||||
(Just <$> readFile path)
|
||||
(\e -> if isDoesNotExistError e then return Nothing else return Nothing)
|
||||
return result
|
||||
|
||||
-- | 字符串居中
|
||||
center :: Int -> String -> String
|
||||
center width s =
|
||||
let padding = max 0 (width - length s)
|
||||
leftPad = padding `div` 2
|
||||
rightPad = padding - leftPad
|
||||
in replicate leftPad ' ' ++ s ++ replicate rightPad ' '
|
||||
|
||||
-- | 重复字符串
|
||||
repeatString :: Int -> String -> String
|
||||
repeatString n = concat . replicate n
|
||||
|
||||
-- | 高亮文本(终端颜色)
|
||||
highlight :: String -> String
|
||||
highlight s = "\ESC[1m" ++ s ++ "\ESC[0m"
|
||||
|
||||
-- | 绿色文本
|
||||
green :: String -> String
|
||||
green s = "\ESC[32m" ++ s ++ "\ESC[0m"
|
||||
|
||||
-- | 黄色文本
|
||||
yellow :: String -> String
|
||||
yellow s = "\ESC[33m" ++ s ++ "\ESC[0m"
|
||||
|
||||
-- | 红色文本
|
||||
red :: String -> String
|
||||
red s = "\ESC[31m" ++ s ++ "\ESC[0m"
|
||||
|
||||
-- | 蓝色文本
|
||||
blue :: String -> String
|
||||
blue s = "\ESC[34m" ++ s ++ "\ESC[0m"
|
||||
92
quickjump.cabal
Normal file
92
quickjump.cabal
Normal file
@@ -0,0 +1,92 @@
|
||||
cabal-version: 3.4
|
||||
-- The cabal-version field refers to the version of the .cabal specification,
|
||||
-- and can be different from the cabal-install (the tool) version and the
|
||||
-- Cabal (the library) version you are using. As such, the Cabal (the library)
|
||||
-- version used must be equal or greater than the version stated in this field.
|
||||
-- Starting from the specification version 2.2, the cabal-version field must be
|
||||
-- the first thing in the cabal file.
|
||||
|
||||
-- Initial package description 'quickjump' generated by
|
||||
-- 'cabal init'. For further documentation, see:
|
||||
-- http://haskell.org/cabal/users-guide/
|
||||
--
|
||||
-- The name of the package.
|
||||
name: quickjump
|
||||
|
||||
-- The package version.
|
||||
-- See the Haskell package versioning policy (PVP) for standards
|
||||
-- guiding when and how versions should be incremented.
|
||||
-- https://pvp.haskell.org
|
||||
-- PVP summary: +-+------- breaking API changes
|
||||
-- | | +----- non-breaking API additions
|
||||
-- | | | +--- code changes with no API change
|
||||
version: 0.3.0.1
|
||||
|
||||
-- A short (one-line) description of the package.
|
||||
synopsis: Directory Jump and Quick Directory Open
|
||||
|
||||
-- A longer description of the package.
|
||||
description: A command line tool for fast directory navigation and configuration management
|
||||
|
||||
-- URL for the project homepage or repository.
|
||||
homepage: git.lyz.one/sidneyzhang
|
||||
|
||||
-- The license under which the package is released.
|
||||
license: MIT
|
||||
|
||||
-- The file containing the license text.
|
||||
license-file: LICENSE
|
||||
|
||||
-- The package author(s).
|
||||
author: Sidney Zhang
|
||||
|
||||
-- An email address to which users can send suggestions, bug reports, and patches.
|
||||
maintainer: zly@lyzhang.me
|
||||
|
||||
-- A copyright notice.
|
||||
-- copyright:
|
||||
category: System
|
||||
build-type: Simple
|
||||
|
||||
-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.
|
||||
extra-doc-files: CHANGELOG.md
|
||||
|
||||
-- Extra source files to be distributed with the package, such as examples, or a tutorial module.
|
||||
-- extra-source-files:
|
||||
|
||||
common warnings
|
||||
ghc-options: -Wall
|
||||
|
||||
executable quickjump
|
||||
-- Import common warning flags.
|
||||
import: warnings
|
||||
|
||||
-- .hs or .lhs file containing the Main module.
|
||||
main-is: Main.hs
|
||||
|
||||
-- Modules included in this executable, other than Main.
|
||||
other-modules: Commands
|
||||
, Config
|
||||
, Types
|
||||
, Utils
|
||||
|
||||
-- LANGUAGE extensions used by modules in this package.
|
||||
-- other-extensions:
|
||||
|
||||
-- Other library packages from which modules are imported.
|
||||
build-depends: aeson ^>=2.2.3.0
|
||||
, aeson-pretty ^>=0.8.10
|
||||
, base ^>=4.18.3.0
|
||||
, bytestring ^>=0.11.5.0
|
||||
, containers ^>=0.6.7
|
||||
, directory ^>=1.3.8.0
|
||||
, filepath ^>=1.4.300.0
|
||||
, optparse-applicative ^>=0.18.1.0
|
||||
, process ^>=1.6.18.0
|
||||
, text ^>=2.0.2
|
||||
|
||||
-- Directories containing source files.
|
||||
hs-source-dirs: app
|
||||
|
||||
-- Base language which the package is written in.
|
||||
default-language: Haskell2010
|
||||
Reference in New Issue
Block a user