feat: interactive mode
This commit is contained in:
parent
f00691ae40
commit
41a8d1a75b
6 changed files with 144 additions and 19 deletions
9
.github/workflows/quality.yml
vendored
9
.github/workflows/quality.yml
vendored
|
|
@ -29,8 +29,9 @@ jobs:
|
||||||
- '.luarc.json'
|
- '.luarc.json'
|
||||||
- '*.toml'
|
- '*.toml'
|
||||||
python:
|
python:
|
||||||
- 'scrapers/**'
|
- 'scripts/**/.py'
|
||||||
- 'tests/scrapers/**'
|
- 'scrapers/**/*.py'
|
||||||
|
- 'tests/**/*.py'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
markdown:
|
markdown:
|
||||||
|
|
@ -103,7 +104,7 @@ jobs:
|
||||||
- name: Install ruff
|
- name: Install ruff
|
||||||
run: uv tool install ruff
|
run: uv tool install ruff
|
||||||
- name: Lint Python files with ruff
|
- name: Lint Python files with ruff
|
||||||
run: ruff check scrapers/ tests/scrapers/
|
run: ruff check scripts/ scrapers/ tests/scrapers/
|
||||||
|
|
||||||
python-typecheck:
|
python-typecheck:
|
||||||
name: Python Type Check
|
name: Python Type Check
|
||||||
|
|
@ -117,7 +118,7 @@ jobs:
|
||||||
- name: Install dependencies with mypy
|
- name: Install dependencies with mypy
|
||||||
run: uv sync --dev
|
run: uv sync --dev
|
||||||
- name: Type check Python files with mypy
|
- name: Type check Python files with mypy
|
||||||
run: uv run mypy scrapers/ tests/scrapers/
|
run: uv run mypy scripts/ scrapers/ tests/scrapers/
|
||||||
|
|
||||||
markdown-format:
|
markdown-format:
|
||||||
name: Markdown Format Check
|
name: Markdown Format Check
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ local actions = constants.ACTIONS
|
||||||
---@field contest? string
|
---@field contest? string
|
||||||
---@field platform? string
|
---@field platform? string
|
||||||
---@field problem_id? string
|
---@field problem_id? string
|
||||||
|
---@field interactor_cmd? string
|
||||||
|
|
||||||
--- Turn raw args into normalized structure to later dispatch
|
--- Turn raw args into normalized structure to later dispatch
|
||||||
---@param args string[] The raw command-line mode args
|
---@param args string[] The raw command-line mode args
|
||||||
|
|
@ -32,7 +33,7 @@ local function parse_command(args)
|
||||||
if first == 'cache' then
|
if first == 'cache' then
|
||||||
local subcommand = args[2]
|
local subcommand = args[2]
|
||||||
if not subcommand then
|
if not subcommand then
|
||||||
return { type = 'error', message = 'cache command requires subcommand: clear' }
|
return { type = 'error', message = 'cache command requires subcommand' }
|
||||||
end
|
end
|
||||||
if vim.tbl_contains({ 'clear', 'read' }, subcommand) then
|
if vim.tbl_contains({ 'clear', 'read' }, subcommand) then
|
||||||
local platform = args[3]
|
local platform = args[3]
|
||||||
|
|
@ -44,6 +45,13 @@ local function parse_command(args)
|
||||||
else
|
else
|
||||||
return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand }
|
return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand }
|
||||||
end
|
end
|
||||||
|
elseif first == 'interact' then
|
||||||
|
local inter = args[2]
|
||||||
|
if inter and inter ~= '' then
|
||||||
|
return { type = 'action', action = 'interact', interactor_cmd = inter }
|
||||||
|
else
|
||||||
|
return { type = 'action', action = 'interact' }
|
||||||
|
end
|
||||||
else
|
else
|
||||||
return { type = 'action', action = first }
|
return { type = 'action', action = first }
|
||||||
end
|
end
|
||||||
|
|
@ -99,7 +107,7 @@ function M.handle_command(opts)
|
||||||
local ui = require('cp.ui.panel')
|
local ui = require('cp.ui.panel')
|
||||||
|
|
||||||
if cmd.action == 'interact' then
|
if cmd.action == 'interact' then
|
||||||
ui.toggle_interactive()
|
ui.toggle_interactive(cmd.interactor_cmd)
|
||||||
elseif cmd.action == 'run' then
|
elseif cmd.action == 'run' then
|
||||||
ui.toggle_run_panel()
|
ui.toggle_run_panel()
|
||||||
elseif cmd.action == 'debug' then
|
elseif cmd.action == 'debug' then
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ function M.disable()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.toggle_interactive()
|
---@param interactor_cmd? string
|
||||||
|
function M.toggle_interactive(interactor_cmd)
|
||||||
if state.get_active_panel() == 'interactive' then
|
if state.get_active_panel() == 'interactive' then
|
||||||
if state.interactive_buf and vim.api.nvim_buf_is_valid(state.interactive_buf) then
|
if state.interactive_buf and vim.api.nvim_buf_is_valid(state.interactive_buf) then
|
||||||
local job = vim.b[state.interactive_buf].terminal_job_id
|
local job = vim.b[state.interactive_buf].terminal_job_id
|
||||||
|
|
@ -42,7 +43,6 @@ function M.toggle_interactive()
|
||||||
state.saved_interactive_session = nil
|
state.saved_interactive_session = nil
|
||||||
end
|
end
|
||||||
state.set_active_panel(nil)
|
state.set_active_panel(nil)
|
||||||
logger.log('Interactive panel closed.')
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -52,12 +52,10 @@ function M.toggle_interactive()
|
||||||
end
|
end
|
||||||
|
|
||||||
local platform, contest_id = state.get_platform(), state.get_contest_id()
|
local platform, contest_id = state.get_platform(), state.get_contest_id()
|
||||||
|
|
||||||
if not platform then
|
if not platform then
|
||||||
logger.log('No platform configured.', vim.log.levels.ERROR)
|
logger.log('No platform configured.', vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if not contest_id then
|
if not contest_id then
|
||||||
logger.log(
|
logger.log(
|
||||||
('No contest %s configured for platform %s.'):format(contest_id, platform),
|
('No contest %s configured for platform %s.'):format(contest_id, platform),
|
||||||
|
|
@ -68,7 +66,7 @@ function M.toggle_interactive()
|
||||||
|
|
||||||
local problem_id = state.get_problem_id()
|
local problem_id = state.get_problem_id()
|
||||||
if not problem_id then
|
if not problem_id then
|
||||||
logger.log(('No problem found for the current problem id %s'):format(problem_id))
|
logger.log('No problem is active.', vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -76,10 +74,12 @@ function M.toggle_interactive()
|
||||||
cache.load()
|
cache.load()
|
||||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||||
if
|
if
|
||||||
contest_data
|
not contest_data
|
||||||
and not contest_data.problems[contest_data.index_map[state.get_problem_id()]].interactive
|
or not contest_data.index_map
|
||||||
|
or not contest_data.problems[contest_data.index_map[problem_id]]
|
||||||
|
or not contest_data.problems[contest_data.index_map[problem_id]].interactive
|
||||||
then
|
then
|
||||||
logger.log('This is NOT an interactive problem. Use :CP run instead.', vim.log.levels.WARN)
|
logger.log('This problem is not interactive. Use :CP run.', vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -95,20 +95,48 @@ function M.toggle_interactive()
|
||||||
end
|
end
|
||||||
|
|
||||||
local binary = state.get_binary_file()
|
local binary = state.get_binary_file()
|
||||||
if not binary then
|
if not binary or binary == '' then
|
||||||
logger.log('no binary path found', vim.log.levels.ERROR)
|
logger.log('No binary produced.', vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.cmd('terminal')
|
local cmdline
|
||||||
|
if interactor_cmd and interactor_cmd ~= '' then
|
||||||
|
local interactor = interactor_cmd
|
||||||
|
if not interactor:find('/') then
|
||||||
|
interactor = './' .. interactor
|
||||||
|
end
|
||||||
|
if vim.fn.executable(interactor) ~= 1 then
|
||||||
|
logger.log(('Interactor not executable: %s'):format(interactor_cmd), vim.log.levels.ERROR)
|
||||||
|
if state.saved_interactive_session then
|
||||||
|
vim.cmd(('source %s'):format(state.saved_interactive_session))
|
||||||
|
vim.fn.delete(state.saved_interactive_session)
|
||||||
|
state.saved_interactive_session = nil
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local orchestrator = vim.fn.fnamemodify(utils.get_plugin_path() .. '/scripts/interact.py', ':p')
|
||||||
|
cmdline = table.concat({
|
||||||
|
'uv',
|
||||||
|
'run',
|
||||||
|
vim.fn.shellescape(orchestrator),
|
||||||
|
vim.fn.shellescape(interactor),
|
||||||
|
vim.fn.shellescape(binary),
|
||||||
|
}, ' ')
|
||||||
|
else
|
||||||
|
cmdline = vim.fn.shellescape(binary)
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.cmd('terminal ' .. cmdline)
|
||||||
local term_buf = vim.api.nvim_get_current_buf()
|
local term_buf = vim.api.nvim_get_current_buf()
|
||||||
local term_win = vim.api.nvim_get_current_win()
|
local term_win = vim.api.nvim_get_current_win()
|
||||||
|
|
||||||
vim.fn.chansend(vim.b.terminal_job_id, binary .. '\n')
|
|
||||||
|
|
||||||
vim.keymap.set('t', '<c-q>', function()
|
vim.keymap.set('t', '<c-q>', function()
|
||||||
M.toggle_interactive()
|
M.toggle_interactive()
|
||||||
end, { buffer = term_buf, silent = true })
|
end, { buffer = term_buf, silent = true })
|
||||||
|
vim.keymap.set('n', '<c-q>', function()
|
||||||
|
M.toggle_interactive()
|
||||||
|
end, { buffer = term_buf, silent = true })
|
||||||
|
|
||||||
state.interactive_buf = term_buf
|
state.interactive_buf = term_buf
|
||||||
state.interactive_win = term_win
|
state.interactive_win = term_win
|
||||||
|
|
|
||||||
|
|
@ -228,4 +228,27 @@ function M.timeout_capability()
|
||||||
return { ok = path ~= nil, path = path, reason = reason }
|
return { ok = path ~= nil, path = path, reason = reason }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function M.cwd_executables()
|
||||||
|
local uv = vim.uv or vim.loop
|
||||||
|
local req = uv.fs_scandir('.')
|
||||||
|
if not req then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local out = {}
|
||||||
|
while true do
|
||||||
|
local name, t = uv.fs_scandir_next(req)
|
||||||
|
if not name then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if t == 'file' or t == 'link' then
|
||||||
|
local path = './' .. name
|
||||||
|
if vim.fn.executable(path) == 1 then
|
||||||
|
out[#out + 1] = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(out)
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ if vim.g.loaded_cp then
|
||||||
end
|
end
|
||||||
vim.g.loaded_cp = 1
|
vim.g.loaded_cp = 1
|
||||||
|
|
||||||
|
local utils = require('cp.utils')
|
||||||
|
|
||||||
vim.api.nvim_create_user_command('CP', function(opts)
|
vim.api.nvim_create_user_command('CP', function(opts)
|
||||||
local cp = require('cp')
|
local cp = require('cp')
|
||||||
cp.handle_command(opts)
|
cp.handle_command(opts)
|
||||||
|
|
@ -51,6 +53,11 @@ end, {
|
||||||
return vim.tbl_filter(function(cmd)
|
return vim.tbl_filter(function(cmd)
|
||||||
return cmd:find(ArgLead, 1, true) == 1
|
return cmd:find(ArgLead, 1, true) == 1
|
||||||
end, { 'clear', 'read' })
|
end, { 'clear', 'read' })
|
||||||
|
elseif args[2] == 'interact' then
|
||||||
|
local cands = utils.cwd_executables()
|
||||||
|
return vim.tbl_filter(function(cmd)
|
||||||
|
return cmd:find(ArgLead, 1, true) == 1
|
||||||
|
end, cands)
|
||||||
end
|
end
|
||||||
elseif num_args == 4 then
|
elseif num_args == 4 then
|
||||||
if args[2] == 'cache' and args[3] == 'clear' then
|
if args[2] == 'cache' and args[3] == 'clear' then
|
||||||
|
|
|
||||||
58
scripts/interact.py
Normal file
58
scripts/interact.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
|
||||||
|
async def pump(
|
||||||
|
reader: asyncio.StreamReader, writer: asyncio.StreamWriter | None
|
||||||
|
) -> None:
|
||||||
|
while True:
|
||||||
|
data = await reader.readline()
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
sys.stdout.buffer.write(data)
|
||||||
|
sys.stdout.flush()
|
||||||
|
if writer:
|
||||||
|
writer.write(data)
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
|
||||||
|
async def main(interactor_cmd: Sequence[str], interactee_cmd: Sequence[str]) -> None:
|
||||||
|
interactor = await asyncio.create_subprocess_exec(
|
||||||
|
*interactor_cmd,
|
||||||
|
stdin=asyncio.subprocess.PIPE,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
interactee = await asyncio.create_subprocess_exec(
|
||||||
|
*interactee_cmd,
|
||||||
|
stdin=asyncio.subprocess.PIPE,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
interactor.stdout
|
||||||
|
and interactor.stdin
|
||||||
|
and interactee.stdout
|
||||||
|
and interactee.stdin
|
||||||
|
)
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
asyncio.create_task(pump(interactor.stdout, interactee.stdin)),
|
||||||
|
asyncio.create_task(pump(interactee.stdout, interactor.stdin)),
|
||||||
|
]
|
||||||
|
await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
|
||||||
|
await interactor.wait()
|
||||||
|
await interactee.wait()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("Usage: interact.py <interactor> <interactee>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
interactor_cmd = shlex.split(sys.argv[1])
|
||||||
|
interactee_cmd = shlex.split(sys.argv[2])
|
||||||
|
|
||||||
|
asyncio.run(main(interactor_cmd, interactee_cmd))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue