Skip to content

Commit e8c65c6

Browse files
committed
feat: added picker option for telescope integration and selection caret to indicate selected task
1 parent d1dfafd commit e8c65c6

14 files changed

Lines changed: 500 additions & 12 deletions

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A simple plugin for [taskfiles](https://taskfile.dev/)
88
## Features
99

1010
- Run a specific task directly within Neovim
11-
- Browse available tasks with a floating window
11+
- Browse available tasks with a floating window **or** [Telescope](https://github.com/nvim-telescope/telescope.nvim)
1212
- Preview each task’s command before execution
1313
- Run tasks in a floating terminal
1414
- Automatically scroll to bottom of output (optional)
@@ -24,6 +24,7 @@ A simple plugin for [taskfiles](https://taskfile.dev/)
2424

2525
- [task](https://taskfile.dev/#/installation) CLI installed and in your `$PATH`
2626
- Neovim 0.8 or higher (0.9+ recommended)
27+
- [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) (optional, only if using `picker = "telescope"`)
2728

2829
## Setup
2930

@@ -43,6 +44,7 @@ All fields are optional and shown below with their default values:
4344

4445
```lua
4546
require('taskfile').setup({
47+
picker = "native", -- Selection UI: 'native' (default) or 'telescope'
4648
layout = "horizontal", -- Layout: 'h', 'horiz', 'horizontal' or 'v', 'vert', 'vertical'.
4749
-- For 'horizontal' layout, list and preview are side-by-side.
4850
-- For 'vertical', list is above preview (vertically stacked).
@@ -96,6 +98,10 @@ You can also bind a key to rerun using the `keymaps.rerun` config.
9698

9799
## Demo
98100

99-
![Demo GIF](./demo/demo.gif)
101+
Demo with Native (default) Picker GIF
102+
![Demo with Native Picker GIF](./demo/demo_native.gif)
103+
104+
Demo with Telescope Picker GIF
105+
![Demo with Telescope Picker GIF](./demo/demo_telescope.gif)
100106

101107
<!-- panvimdoc-ignore-end -->
File renamed without changes.

demo/demo.tape renamed to demo/demo_native.tape

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Output demo.gif
1+
Output demo_native.gif
22

33
Set Theme "TokyoNight"
44

@@ -20,6 +20,13 @@ Enter
2020
Type "nvim app.go"
2121
Enter
2222

23+
Type ":lua require('taskfile').setup({ picker = 'native', layout = 'horizontal', windows = { output = { width = 0.8, height = 0.8, }, list = { width = 0.8, height = 0.8, }, })"
24+
Enter
25+
26+
Sleep 500ms
27+
Type ":"
28+
Backspace
29+
2330
Show
2431
Sleep 750ms
2532
Type ":Task"

demo/demo_telescope.gif

1 MB
Loading

demo/demo_telescope.tape

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
Output demo_telescope.gif
2+
3+
Set Theme "TokyoNight"
4+
5+
Set Padding 5
6+
Set Margin 10
7+
Set MarginFill "#3D4262"
8+
Set BorderRadius 7
9+
10+
Set Width 1440
11+
Set Height 900
12+
Set FontSize 16
13+
14+
Hide
15+
Type "cd $HOME/git/examples/go-web-app"
16+
Enter
17+
Type "echo $PWD"
18+
Enter
19+
Type "nvim app.go"
20+
Enter
21+
22+
Type ":lua require('taskfile').setup({ picker = 'telescope', layout = 'vertical', windows = { output = { width = 0.8, height = 0.8, }, list = { width = 0.8, height = 0.8, }, })"
23+
Enter
24+
25+
Sleep 500ms
26+
Type ":"
27+
Backspace
28+
29+
Show
30+
Sleep 750ms
31+
Type ":Task"
32+
Sleep 750ms
33+
Enter
34+
Sleep 750ms
35+
36+
Escape
37+
Type ":TaskToggleLayout"
38+
Sleep 500ms
39+
Enter
40+
Sleep 500ms
41+
42+
Type "clean"
43+
Sleep 1000ms
44+
Enter
45+
Sleep 1000ms
46+
Type "q"
47+
Sleep 1000ms
48+
49+
50+
Type ":Task"
51+
Sleep 1000ms
52+
Enter
53+
Sleep 1000ms
54+
55+
Ctrl+n
56+
Sleep 500ms
57+
Ctrl+n
58+
Sleep 500ms
59+
Enter
60+
Sleep 1000ms
61+
62+
Type "q"
63+
Sleep 1000ms
64+
65+
Space
66+
Sleep 1000ms
67+
Type "t"
68+
Sleep 1000ms
69+
Type "r"
70+
Sleep 1000ms

lua/taskfile.lua

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ M.execute_or_select = function(task)
3434
return
3535
end
3636

37+
local opts = core._options or {}
38+
39+
if opts.picker == "telescope" then
40+
-- Try to load telescope extension
41+
local ok, telescope_ext = pcall(require, "taskfile.telescope")
42+
if ok then
43+
telescope_ext.pick_task(tasks, core.execute_task, opts)
44+
return
45+
else
46+
vim.notify("Telescope not installed. Falling back to native UI.", vim.log.levels.WARN)
47+
end
48+
end
49+
3750
local preview_win_cfg = core.get_list_config()
3851
ui.select_task_with_preview(tasks, preview_win_cfg)
3952
end

lua/taskfile/core.lua

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ local utils = require("taskfile.utils")
2828
---@field rerun? string mapping to rerun last task
2929

3030
---@class TaskfileConfig
31+
---@field picker? "native"|"telescope" Selection UI to use (default: "native")
3132
---@field layout? TaskfileLayoutInput|string default selector UI layout; accepts shorthands, normalized internally.
3233
---@field windows? TaskfileWindowsConfig floating window layout options.
3334
---@field scroll? TaskfileScrollConfig output scroll behavior
@@ -36,6 +37,7 @@ local utils = require("taskfile.utils")
3637
--- default configuration
3738
---@type TaskfileConfig
3839
local config = {
40+
picker = "native",
3941
layout = "horizontal",
4042
windows = {
4143
output = { width = 0.8, height = 0.8, border = "rounded" },
@@ -138,6 +140,16 @@ M.setup = function(opts)
138140
})
139141
M._options = vim.tbl_deep_extend("force", {}, config, opts or {})
140142

143+
vim.validate({
144+
picker = {
145+
M._options.picker,
146+
function(p)
147+
return p == "native" or p == "telescope"
148+
end,
149+
'must be "native" or "telescope"',
150+
},
151+
})
152+
141153
M._options.layout = utils.normalize_layout(M._options.layout)
142154

143155
utils.validate_range({

lua/taskfile/telescope.lua

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
--@class telescope
2+
local M = {}
3+
4+
local pickers = require("telescope.pickers")
5+
local finders = require("telescope.finders")
6+
local conf = require("telescope.config").values
7+
local actions = require("telescope.actions")
8+
local action_state = require("telescope.actions.state")
9+
local previewers = require("telescope.previewers")
10+
local putils = require("telescope.previewers.utils")
11+
12+
local utils = require("taskfile.utils")
13+
local ui = require("taskfile.ui")
14+
15+
local C = utils.const
16+
17+
-- cache constants
18+
local TASK_NAME_DESC_GAP = C.TASK_NAME_DESC_GAP
19+
local MIN_PREVIEW_HEIGHT = C.MIN_PREVIEW_HEIGHT
20+
local MIN_PREVIEW_WIDTH = C.MIN_PREVIEW_WIDTH
21+
local NO_WRAP_WIDTH = C.NO_WRAP_WIDTH
22+
local TELESCOPE_HEIGHT = C.TELESCOPE_HEIGHT
23+
local TELESCOPE_WIDTH = C.TELESCOPE_WIDTH
24+
local SELECTION_CARET = C.SELECTION_CARET
25+
26+
M.const = C
27+
28+
---@class TelescopeLayoutConfig
29+
---@field width integer Total window width
30+
---@field height integer Total window height
31+
---@field preview_width? integer Horizontal: Specific width for preview
32+
---@field preview_height? integer Vertical: Specific height for preview
33+
---@field prompt_position? "top"|"bottom"
34+
---@field mirror? boolean If true, mirrors the layout (prompt/list on top in vertical)
35+
---@field label_width? integer Cached max width of task labels
36+
37+
--- Calculate layout to match native behavior
38+
---@param opts table Taskfile configuration
39+
---@param tasks table List of tasks
40+
---@return string strategy, TelescopeLayoutConfig layout_conf
41+
local function get_layout_config(opts, tasks)
42+
opts = opts or {}
43+
local list_conf = (opts.windows and opts.windows.list) or {}
44+
45+
local strategy = opts.layout or "horizontal"
46+
47+
local total_width, total_height, _, _ = utils.calculate_dimensions(list_conf.width, list_conf.height)
48+
local label_width = utils.max_task_label_length(tasks)
49+
50+
---@type TelescopeLayoutConfig
51+
local layout_conf = {
52+
width = total_width,
53+
height = total_height,
54+
label_width = label_width,
55+
}
56+
57+
if strategy == "vertical" then
58+
layout_conf.mirror = true
59+
layout_conf.prompt_position = "bottom"
60+
61+
local available_content_h = math.max(0, total_height - TELESCOPE_HEIGHT)
62+
63+
local list_h =
64+
ui.calculate_list_height(tasks, list_conf.height_ratio, available_content_h, total_width, label_width)
65+
local preview_h = available_content_h - list_h
66+
67+
if preview_h < MIN_PREVIEW_HEIGHT then
68+
preview_h = MIN_PREVIEW_HEIGHT
69+
end
70+
71+
layout_conf.preview_height = preview_h
72+
else
73+
layout_conf.prompt_position = "bottom"
74+
75+
local available_content_w = math.max(0, total_width - TELESCOPE_WIDTH)
76+
local list_w = ui.calculate_list_width(tasks, list_conf.width_ratio, available_content_w, label_width)
77+
78+
local preview_w = available_content_w - list_w
79+
layout_conf.preview_width = math.max(MIN_PREVIEW_WIDTH, preview_w)
80+
end
81+
82+
return strategy, layout_conf
83+
end
84+
85+
local function task_previewer()
86+
return previewers.new_buffer_previewer({
87+
title = "Preview",
88+
define_preview = function(self, entry)
89+
local cmd = "task " .. entry.value.name .. " --dry"
90+
local output = vim.fn.system(cmd)
91+
local lines = vim.split(output, "\n")
92+
local cleaned_lines = utils.clean_dry_output(lines)
93+
94+
vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, cleaned_lines)
95+
putils.highlighter(self.state.bufnr, "sh")
96+
end,
97+
})
98+
end
99+
100+
M.pick_task = function(tasks, run_callback, plugin_opts)
101+
local strategy, layout_conf = get_layout_config(plugin_opts, tasks)
102+
103+
pickers
104+
.new({}, {
105+
prompt_title = "Find Task",
106+
results_title = "Tasks",
107+
selection_caret = SELECTION_CARET,
108+
109+
finder = finders.new_table({
110+
results = tasks,
111+
entry_maker = function(task)
112+
local name = task.name or ""
113+
local desc = task.desc or ""
114+
115+
-- we pass NO_WRAP_WIDTH to effectively disable wrapping.
116+
local formatted_lines =
117+
utils.format_task_lines(name, desc, layout_conf.label_width, NO_WRAP_WIDTH, TASK_NAME_DESC_GAP)
118+
119+
return {
120+
value = task,
121+
display = formatted_lines[1],
122+
ordinal = name .. " " .. desc,
123+
}
124+
end,
125+
}),
126+
127+
sorter = conf.generic_sorter({}),
128+
previewer = task_previewer(),
129+
130+
layout_strategy = strategy,
131+
layout_config = layout_conf,
132+
133+
attach_mappings = function(prompt_bufnr)
134+
actions.select_default:replace(function()
135+
actions.close(prompt_bufnr)
136+
local selection = action_state.get_selected_entry()
137+
if selection and selection.value then
138+
run_callback(selection.value.name)
139+
end
140+
end)
141+
return true
142+
end,
143+
})
144+
:find()
145+
end
146+
147+
return M

0 commit comments

Comments
 (0)