Skip to content

Командная строка: Ctrl+Alt+. вставляет последний аргумент предыдущей команды (как в bash) - #3519

Open
simwin wants to merge 6 commits into
elfmz:masterfrom
simwin:alt-dot-yank-last-arg

Conversation

@simwin

@simwin simwin commented Aug 6, 2026

Copy link
Copy Markdown

Что это

Alt+. в командной строке — как в bash/readline (yank-last-arg): вставляет в позицию курсора последний аргумент предыдущей команды; каждое следующее нажатие Alt+. замещает вставленное последним аргументом всё более старой команды из истории.

echo hello_from_history
ls /tmp/somedir
cp <Alt+.>        ->  cp /tmp/somedir
   <Alt+.>        ->  cp hello_from_history

Очень привычный из шелла приём, особенно экономит время на длинных путях.

Реализация

  • History::GetRecentStr(Index, strStr) — доступ к N-й записи истории с конца. Не использует и не меняет CurrentItem, поэтому навигация Ctrl-E/Ctrl-X не затрагивается.
  • CommandLine::ProcessKey_YankLastArg() — извлечение последнего аргумента (аргументы в кавычках "..."/'...' берутся целиком) и циклирование; состояние цикла сбрасывается любой другой клавишей. Состояние Ctrl-End (LastCmdPartLength) при изменении строки инвалидируется.
  • Разрешение конфликта с fast-find: Alt+. уходит в командную строку только когда она не пуста — по образцу уже существующей обработки Ctrl-D в filepanels.cpp (diff при непустой строке / cmdline иначе). При пустой командной строке прежнее поведение Alt+. (fast-find по именам на точку) полностью сохранено.
  • KEY_ALTDOT уже был определён в farkeys.h и декодировался из ESC-последовательности TTY (console/keyboard.cpp), но ни к чему не был привязан.

Тестирование

  • Сборка: Linux, TTY backend — ок.
  • Автотест на pty (pyte эмулирует терминал и рендерит экран):
OK   first  Alt+. -> ~# cp /tmp/far2l_altdot_test_m8dgxmwn
OK   second Alt+. -> ~# cp hello_from_history
  • Вручную проверено: циклирование, сброс цикла другой клавишей, вставка не в конце строки, кавычки, fast-find по Alt+. при пустой строке не изменился, Ctrl-E/Ctrl-X работают как раньше.
Скрипт автотеста (python3, требует pyte)
#!/usr/bin/env python3
# Automated pty test for far2l Alt+. (yank-last-arg) feature.
# Usage: python3 test_altdot_pty.py /path/to/far2l
# Requires: pip install pyte
import os, pty, select, time, fcntl, termios, struct, sys, tempfile

import pyte

FAR2L = sys.argv[1] if len(sys.argv) > 1 else 'far2l'
ROWS, COLS = 30, 100

screen = pyte.Screen(COLS, ROWS)
stream = pyte.ByteStream(screen)

home = tempfile.mkdtemp(prefix='far2l_altdot_test_')

pid, fd = pty.fork()
if pid == 0:
    os.environ['TERM'] = 'xterm-256color'
    os.environ['HOME'] = home
    os.execv(FAR2L, ['far2l', '--tty'])

fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', ROWS, COLS, 0, 0))

def feed(timeout=2.0):
    end = time.time() + timeout
    while time.time() < end:
        r, _, _ = select.select([fd], [], [], 0.3)
        if r:
            try:
                d = os.read(fd, 65536)
            except OSError:
                return
            if not d:
                return
            stream.feed(d)

def keybar_visible():
    return screen.display[ROWS - 1].lstrip().startswith('1')

def cmdline():
    # the line right above the key bar
    return screen.display[ROWS - 2].rstrip()

feed(4.0)
# wait for panels; answer "revive or spawn new" prompt if it shows up
for _ in range(10):
    if keybar_visible():
        break
    if any('Input instance index' in l for l in screen.display):
        os.write(fd, b'\r')
    feed(2.0)
else:
    print('FAIL: panels did not appear')
    for i, line in enumerate(screen.display):
        if line.strip():
            print(f'{i:2}| {line.rstrip()}')
    sys.exit(1)

os.write(fd, b'echo hello_from_history\r'); feed(3.0)
os.write(fd, b'ls ' + home.encode() + b'\r'); feed(3.0)

os.write(fd, b'cp '); feed(1.0)
os.write(fd, b'\x1b.'); feed(2.0)   # Alt+. : yank last arg of previous command
first = cmdline()
os.write(fd, b'\x1b.'); feed(2.0)   # Alt+. again: cycle to older command
second = cmdline()

ok1 = ('cp ' + home) in first
ok2 = 'cp hello_from_history' in second
print(('OK  ' if ok1 else 'FAIL') + ' first  Alt+. -> ' + first)
print(('OK  ' if ok2 else 'FAIL') + ' second Alt+. -> ' + second)

try:
    os.kill(pid, 15)
except ProcessLookupError:
    pass
sys.exit(0 if (ok1 and ok2) else 1)

Обновление по ревью (@akruphi): поведение Alt+. сделано настраиваемым — F9 → Параметры → Настройки командной строки, три режима (только при скрытых панелях / при скрытых панелях или непустой строке — по умолчанию / всегда).

Скрипт автотеста трёх режимов (python3, требует pyte)

#!/usr/bin/env python3
# Tests the three modes of "Alt+. inserts last argument" setting in far2l.
# Usage: python3 test_altdot_modes.py /path/to/far2l
import os, pty, select, time, fcntl, termios, struct, sys, shutil

import pyte

FAR2L = sys.argv[1] if len(sys.argv) > 1 else 'far2l'
ROWS, COLS = 30, 100
HOME = '/tmp/far2l_altdot_modes'
results = []


def run_far2l(mode):
    """Starts far2l with AltDotYankLastArg=mode (None = default), returns (pid, fd, screen)."""
    shutil.rmtree(HOME, ignore_errors=True)
    os.makedirs(HOME)
    # far2l resolves profile dir via getpwuid(), not $HOME
    import pwd
    real_home = pwd.getpwuid(os.geteuid()).pw_dir
    cfg = real_home + '/.config/far2l'
    shutil.rmtree(cfg, ignore_errors=True)
    os.makedirs(cfg + '/settings')
    with open(cfg + '/settings/config.ini', 'w') as f:
        # empty section when mode is None: profile exists (no first-start
        # dialogs) but the option keeps its default value
        f.write('[Cmdline]\n' if mode is None else '[Cmdline]\nAltDotYankLastArg=%d\n' % mode)

    screen = pyte.Screen(COLS, ROWS)
    stream = pyte.ByteStream(screen)
    pid, fd = pty.fork()
    if pid == 0:
        os.environ['TERM'] = 'xterm-256color'
        os.environ['HOME'] = HOME
        os.execv(FAR2L, ['far2l', '--tty'])
    fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', ROWS, COLS, 0, 0))

    def feed(timeout=2.0):
        end = time.time() + timeout
        while time.time() < end:
            r, _, _ = select.select([fd], [], [], 0.3)
            if r:
                try:
                    d = os.read(fd, 65536)
                except OSError:
                    return
                if not d:
                    return
                stream.feed(d)

    feed(4.0)
    for _ in range(10):
        if screen.display[ROWS - 1].lstrip().startswith('1'):
            break
        if any('Input instance index' in l for l in screen.display):
            os.write(fd, b'\r')
        feed(2.0)
    else:
        raise RuntimeError('panels did not appear')
    return pid, fd, screen, feed


def cmdline(screen):
    return screen.display[ROWS - 2].rstrip()


def check(name, cond, detail):
    results.append((name, cond, detail))
    print(('OK  ' if cond else 'FAIL') + ' ' + name + ' -> ' + detail)


def stop(pid, fd):
    try:
        os.kill(pid, 9)
    except ProcessLookupError:
        pass
    try:
        os.close(fd)
    except OSError:
        pass


ARG = '/tmp/altdot_probe_dir'
os.makedirs(ARG, exist_ok=True)

# --- default (no config): behaves as mode 1 ---
pid, fd, screen, feed = run_far2l(None)
os.write(fd, b'echo hello_from_history\r'); feed(2.5)
os.write(fd, b'ls ' + ARG.encode() + b'\r'); feed(2.5)
os.write(fd, b'cp '); feed(1.0)
os.write(fd, b'\x1b.'); feed(1.5)
check('default: non-empty line yanks', ('cp ' + ARG) in cmdline(screen), cmdline(screen))
os.write(fd, b'\x1b.'); feed(1.5)
check('default: cycles to older', 'cp hello_from_history' in cmdline(screen), cmdline(screen))
os.write(fd, b'\x1b'); feed(1.0)  # clear line
# empty line -> fast-find, not yank
os.write(fd, b'\x1b.'); feed(1.5)
check('default: empty line no yank', ARG not in cmdline(screen), cmdline(screen) or '<empty>')
os.write(fd, b'\x1b'); feed(1.0)  # close fast-find
stop(pid, fd)

# --- mode 2: always ---
pid, fd, screen, feed = run_far2l(2)
os.write(fd, b'ls ' + ARG.encode() + b'\r'); feed(2.5)
os.write(fd, b'\x1b.'); feed(1.5)   # empty command line
check('always: empty line yanks', ARG in cmdline(screen), cmdline(screen) or '<empty>')
stop(pid, fd)

# --- mode 0: only when panels are hidden ---
pid, fd, screen, feed = run_far2l(0)
os.write(fd, b'ls ' + ARG.encode() + b'\r'); feed(2.5)
os.write(fd, b'cp '); feed(1.0)
os.write(fd, b'\x1b.'); feed(1.5)   # panels visible -> fast-find even with text
check('panels-off: visible panels no yank', ARG not in cmdline(screen), cmdline(screen))
os.write(fd, b'\x1b'); feed(1.0)    # close fast-find
os.write(fd, b'\x0f'); feed(1.5)    # Ctrl-O: hide panels
os.write(fd, b'\x1b.'); feed(1.5)
check('panels-off: hidden panels yank', ('cp ' + ARG) in cmdline(screen), cmdline(screen))
stop(pid, fd)

print('---')
sys.exit(0 if all(c for _, c, _ in results) else 1)

Repeated presses cycle through progressively older history entries, replacing the previously inserted argument, like readline yank-last-arg. With empty command line Alt+. keeps its old panel fast-find behavior.
@akruphi

akruphi commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

В far исторически Alt+символ - начало быстрого поиска файла на панели и скрытые файлы, начинающиеся с точки, частое явление в linux. Поэтому при скрытых панелях предлагаемое использование Alt+. точно полезно. Но при видимости хотя бы одной панели уже неоднозначно - возможно вынести настройку в F9->Options->Panel settings или Command line settings, предлагая выбор:

Alt+. - аргумент последней команды 
* только  при скрытых панелях
* при скрытых панелях или наличии непустого текста в ком. строке
* всегда

Options -> Command line settings, three modes: only when panels are
hidden / when panels are hidden or command line is not empty (default,
matches previous behavior and the Ctrl-D precedent) / always.
Requested in review by akruphi.
@simwin

simwin commented Aug 7, 2026

Copy link
Copy Markdown
Author

Согласен, сделал настройкой. F9 → Параметры → Настройки командной строки, выпадающий список «Alt+. вставляет аргумент последней команды» с тремя вариантами:

  • только при скрытых панелях
  • при скрытых панелях или непустой командной строке
  • всегда

По умолчанию — второй вариант: он повторяет уже существующий в far2l прецедент Ctrl-D (непустая строка → командная строка, иначе панель), и при пустой командной строке быстрый поиск по точке полностью сохраняется. При скрытых панелях (Ctrl-O) вставка аргумента работает во всех трёх режимах — быстрый поиск там всё равно недоступен.

Все три режима покрыты автотестом на pty (обновлённый скрипт добавил в PR): вставка и циклирование при непустой строке, быстрый поиск при пустой, быстрый поиск при видимых панелях в режиме «только при скрытых», вставка после Ctrl-O.

@elfmz

elfmz commented Aug 19, 2026

Copy link
Copy Markdown
Owner

При непустой командной строке тоже не вариант, я например часто начинаю писать команду потом с помощью alt+ нахожу файлы которые вставляю по ctrl+enter. Все же не стоит из фара делать все остальное вместе взятое.

@akruphi

akruphi commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

При непустой командной строке тоже не вариант, я например часто начинаю писать команду потом с помощью alt+ нахожу файлы которые вставляю по ctrl+enter. Все же не стоит из фара делать все остальное вместе взятое.

Тогда вместо Alt+. вешать на Ctrl+Alt+.?

Фича всё-таки полезная.

@simwin

simwin commented Aug 19, 2026

Copy link
Copy Markdown
Author

@elfmz согласен, ваш сценарий (Alt+. → fast-find по файлу при непустой командной строке → Ctrl+Enter вставляет имя, дописанный текст команды остаётся) — это ровно тот кейс, который наш дефолт ломал. Спасибо, что описали конкретно.

@akruphi да, Ctrl+Alt+. — чистое решение, а не просто обходной путь: комбинация в far2l сейчас ничем не занята, и её декодирование уже существует в keyboard.cpp рядом с Ctrl+. / Alt+. / Ctrl+Shift+. (case VK_OEM_PERIOD: return (KEY_CTRL + KEY_ALT + KEY_DOT); в ветке CtrlPressed && AltPressed), нужно только завести именованную константу KEY_CTRLALTDOT по образцу соседних.

Сегодня-завтра пришлю обновление.


Добавлю в справку эту строку:

Insert last argument of previous command (bash-like)      #Ctrl-Alt-.#

Alt+. returns to its historical panel fast-find behavior without exceptions. Ctrl+Alt+. always inserts/cycles the last argument of previous command. Config option and dialog entry removed as no longer needed. Help entry added (Eng/Rus/Ukr/Hun). Requested in review.
@simwin

simwin commented Aug 21, 2026

Copy link
Copy Markdown
Author

Сделано, как договорились:

  • Alt+. возвращён к историческому поведению без каких-либо исключений — добавленный ранее обработчик в filepanels.cpp убран целиком, быстрый поиск по панели работает как до PR, в том числе при непустой командной строке (сценарий из замечания проверен автотестом).
  • Ctrl+Alt+. — выделенная комбинация, всегда вставляет/циклирует последний аргумент предыдущей команды. В farkeys.h добавлена константа KEY_CTRLALTDOT по образцу соседних; keyboard.cpp не тронут — декодирование уже существовало.
  • Диалоговая настройка и опция конфига удалены полностью (config.hpp, ConfigOpt.cpp, config.cpp, языковые ключи). Итог по PR относительно прошлой версии: +12/−85.
  • Справка: строка в F1 → Команды командной строки (Eng/Rus/Ukr/Hun) рядом с Previous/Next command.

Обновлённый автотест (pty, комбинация подаётся kitty-последовательностью \x1b[46;7u): вставка при непустой строке, циклирование, вставка при пустой строке, и возврат Alt+. к быстрому поиску при непустой командной строке.

Замечание о доставке клавиш: в GUI-версии Ctrl+Alt+. работает везде; в TTY-режиме — при kitty keyboard protocol / win32-input-mode / far2l extensions. В «голом» legacy-вводе xterm такая комбинация неотличима от Alt+. — это общее свойство Ctrl+Alt+символ в legacy tty, не специфичное для этой фичи.

обновлённый скрипт автотеста (python3, требует pyte)
#!/usr/bin/env python3
# Tests Ctrl+Alt+. yank-last-arg and that Alt+. is back to panel fast-find.
# Usage: python3 test_ctrlaltdot_pty.py /path/to/far2l
import os, pty, select, time, fcntl, termios, struct, sys, shutil, pwd

import pyte

FAR2L = sys.argv[1] if len(sys.argv) > 1 else 'far2l'
ROWS, COLS = 30, 100
CTRL_ALT_DOT = b'\x1b[46;7u'   # kitty keyboard protocol: '.' (46) + Ctrl+Alt (1+2+4)
ALT_DOT = b'\x1b.'
results = []

real_home = pwd.getpwuid(os.geteuid()).pw_dir
cfg = real_home + '/.config/far2l'
shutil.rmtree(cfg, ignore_errors=True)
os.makedirs(cfg + '/settings')
with open(cfg + '/settings/config.ini', 'w') as f:
    f.write('[Cmdline]\n')   # profile exists -> no first-start dialogs

screen = pyte.Screen(COLS, ROWS)
stream = pyte.ByteStream(screen)
pid, fd = pty.fork()
if pid == 0:
    os.environ['TERM'] = 'xterm-256color'
    os.execv(FAR2L, ['far2l', '--tty'])
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', ROWS, COLS, 0, 0))


def feed(timeout=2.0):
    end = time.time() + timeout
    while time.time() < end:
        r, _, _ = select.select([fd], [], [], 0.3)
        if r:
            try:
                d = os.read(fd, 65536)
            except OSError:
                return
            if not d:
                return
            stream.feed(d)


feed(4.0)
for _ in range(10):
    if screen.display[ROWS - 1].lstrip().startswith('1'):
        break
    if any('Input instance index' in l for l in screen.display):
        os.write(fd, b'\r')
    feed(2.0)
else:
    print('FAIL: panels did not appear')
    sys.exit(1)


def cmdline():
    return screen.display[ROWS - 2].rstrip()


def check(name, cond, detail):
    results.append(cond)
    print(('OK  ' if cond else 'FAIL') + ' ' + name + ' -> ' + detail)


ARG = '/tmp/altdot_probe_dir'
os.makedirs(ARG, exist_ok=True)

os.write(fd, b'echo hello_from_history\r'); feed(2.5)
os.write(fd, b'ls ' + ARG.encode() + b'\r'); feed(2.5)

# 1-2: Ctrl+Alt+. inserts and cycles with non-empty line
os.write(fd, b'cp '); feed(1.0)
os.write(fd, CTRL_ALT_DOT); feed(1.5)
check('Ctrl+Alt+. inserts', ('cp ' + ARG) in cmdline(), cmdline())
os.write(fd, CTRL_ALT_DOT); feed(1.5)
check('Ctrl+Alt+. cycles', 'cp hello_from_history' in cmdline(), cmdline())
os.write(fd, b'\x1b'); feed(1.0)   # clear line

# 3: Ctrl+Alt+. works on empty line too (always-on now)
os.write(fd, CTRL_ALT_DOT); feed(1.5)
check('Ctrl+Alt+. on empty line', ARG in cmdline(), cmdline() or '<empty>')
os.write(fd, b'\x1b'); feed(1.0)

# 4: Alt+. is back to panel fast-find even with non-empty command line
os.write(fd, b'cp '); feed(1.0)
os.write(fd, ALT_DOT); feed(1.5)
fastfind = any(('Поиск' in l) or ('Search' in l) for l in screen.display)
check('Alt+. restored to fast-find', fastfind and ARG not in cmdline(), cmdline())
os.write(fd, b'\x1b'); feed(1.0)

try:
    os.kill(pid, 9)
except ProcessLookupError:
    pass
print('---')
sys.exit(0 if all(results) else 1)

1) wx 3.2.8/GTK does not generate EVT_CHAR for Ctrl+Alt+punctuation, so waiting for OnChar (workaround for wx #23421) swallowed such keystrokes; when Ctrl is pressed, OnKeyDown unicode value is trustworthy - enqueue right away. 2) wxGetKeyState() on wx 3.2.8 may falsely report a modifier as released while the just-received event carries it as pressed, making CheckForSuddenModifiersUp inject phantom modifier-up on key repeats within a held chord; trust the event over the poll. Both bugs affected all Ctrl+Alt+punctuation combos in GUI on recent distros, including the new Ctrl+Alt+. of this PR.
@simwin

simwin commented Aug 21, 2026

Copy link
Copy Markdown
Author

Обновление: при живой проверке фичи в GUI-сборке на wx 3.2.8 (Debian 13) обнаружились и починены два бага wx-бэкенда, из-за которых все комбинации Ctrl+Alt+<знак препинания> (не только новая Ctrl+Alt+.) не работали в GUI на свежих дистрибутивах. Оба не специфичны для этого PR, но без них фича в GUI мертва, поэтому фиксы включены сюда отдельным коммитом — если удобнее вынести в отдельный PR, вынесу.

Баг 1: нажатие тонет в ожидании EVT_CHAR, который не приходит. Для layout-dependent клавиш с зажатым Alt обработчик OnKeyDown не ставит событие в очередь, полагаясь на последующий OnChar (обход wx issue #23421). Но wx 3.2.8/GTK не генерирует EVT_CHAR для Ctrl+Alt+пунктуации вовсе — нажатие пропадает. Подтверждено логами FAR2L_STDERR: OnKeyDown: ACsm "." uni=2e приходит, OnChar за ним — никогда (на wx 3.2.4 OnChar приходит, поэтому там работало). Фикс: при зажатом Ctrl юникод в OnKeyDown достоверен (#23421 касается чистого Alt+нелатиница), ставим событие в очередь сразу из OnKeyDown; задвоения нет — OnChar и так пропускает уже поставленное по _last_keydown_enqueued.

Баг 2: при повторных нажатиях в зажатом аккорде теряется Alt. Симптом-лесенка: первое Ctrl+Alt+. работает, второе — ничего (far2l получает голый Ctrl+. и тихо начинает запись макроса), третье — открывается диалог макрокоманды. Причина в KeyTracker::CheckForSuddenModifierUp: на wx 3.2.8 wxGetKeyState(WXK_ALT) возвращает false при физически зажатом Alt — в тот же момент, когда обрабатываемое событие несёт AltDown()==true. Ревизор верит опросу, вычёркивает Alt из трекера и инжектит фантомный VK_MENU UP; далее wx2INPUT_RECORD для Ctrl+Alt сознательно строит состояние только по трекеру (защита от AltGr-композиций) — и Alt пропадает из очереди. В логе виден и фантомный UP, и финальный OnKeyUp ALT ... UNPAIRED. Фикс: CheckForSuddenModifiersUp() получает текущее событие как подтверждение и не проверяет модификаторы, которые оно сообщает нажатыми — свежее событие достовернее опроса.

Проверено: wx 3.2.8 (Debian 13) — одиночные и повторные Ctrl+Alt+. в зажатом аккорде работают, вставка и циклирование корректны; wx 3.2.4 — регрессий нет (лог: три идентичных LA LC VK_OEM_PERIOD DOWN, ревизор не срабатывает ложно). TTY-режим не затронут (там свой путь ввода, работал и работает).

@simwin

simwin commented Aug 21, 2026

Copy link
Copy Markdown
Author

Уточнение к фразе «TTY-режим не затронут (работал и работает)», чтобы не создать ложных ожиданий: речь о том, что фиксы не задели терминальный путь ввода, и обработка Ctrl+Alt+. в TTY исправна — когда терминал способен передать такую комбинацию (kitty keyboard protocol, far2l extensions — например, вложенный far2l или ssh из far2l). В терминалах с классическим вводом (VTE и т.п.) Ctrl+Alt+. на уровне самого терминала кодируется неотличимо от Alt+. — проверено showkey -a: приходит ^[. — так что там комбинация недоступна, как и упоминалось ранее в треде. Это общее свойство всех Ctrl+Alt+пунктуация в far2l, не специфичное для этой фичи.

@akruphi

akruphi commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

В название PR стоит поправить на актуальный "Ctrl+Alt+."

@simwin simwin changed the title Командная строка: Alt+. вставляет последний аргумент предыдущей команды (как в bash) Командная строка: Ctrl+Alt+. вставляет последний аргумент предыдущей команды (как в bash) Aug 22, 2026
Ctrl+Alt+, cycles backwards through the last-argument history, in case Ctrl+Alt+. was pressed one time too many. Not Ctrl+Alt+Shift+.: on many X11 setups holding Ctrl+Alt triggers AltGr emulation, and the subsequent Shift press is consumed as an ISO_Level3_Shift chooser instead of reaching the application - confirmed by live testing.
@simwin

simwin commented Aug 22, 2026

Copy link
Copy Markdown
Author

Небольшое улучшение по итогам собственного использования: добавил обратное направление циклирования — Ctrl+Alt+, (запятая) — на случай, если Ctrl+Alt+. пролистал дальше, чем нужно.

Комбинация уже декодируется существующим кодом (case VK_OEM_COMMA: return (KEY_CTRL + KEY_ALT + KEY_COMMA);), новых веток декодера не потребовалось. ProcessKey_YankLastArg получил параметр направления: назад — тот же поиск по истории, что и вперёд, но в обратную сторону, с остановкой на самой новой записи (без ухода в отрицательные глубины). Строка в справке добавлена на всех четырёх языках рядом с прямым направлением.

Выбрал именно запятую, а не Shift поверх Ctrl+Alt: на многих X11-системах удержание Ctrl+Alt само по себе эмулирует AltGr, и последующее нажатие Shift перехватывается как переключатель уровня раскладки (ISO_Level3_Shift), не доходя до приложения как обычный модификатор. Запятая не накладывает третий модификатор и работает надёжно везде.

Проверено pty-тестом: три Ctrl+Alt+. дают three → two → one, два Ctrl+Alt+, возвращают two → three, третье нажатие назад (уже на самой новой записи) — no-op, ничего не портит.

@simwin

simwin commented Aug 22, 2026

Copy link
Copy Markdown
Author

Добавил автотест на всякий случай, если кто-то захочет проверить фичу независимо.

Написал и в формате штатного JS-харнесса far2l (testing/tests/), и как отдельный python+pty скрипт. С JS-версией не сложилось: собственный тестовый харнесс на этой ветке (testing/go.mod зафиксирован на Go 1.18 и зависимостях 2024 года) у меня падает уже на штатном, не тронутом нами 0001-run-and-exitExpectString("Do you want to quit FAR?") не находит диалог подтверждения выхода при сборке текущим Go (1.24). Похоже на несовместимость версий в тестовой инфраструктуре, никак не связанную с этим PR — до нашего 0007 дело просто не доходит. Не стал коммитить непроверенный JS-тест в общую тестовую директорию, чтобы не создавать путаницу.

Вместо этого — отдельный воспроизводимый скрипт (python3, требует pyte: pip install pyte), проверяющий все режимы: вставку/циклирование вперёд, циклирование назад, no-op на границе истории, и что Alt+. не тронут:

test_yank_last_arg.py
#!/usr/bin/env python3
"""
Automated pty test for the "bash-like yank-last-arg" cmdline feature:

  Ctrl+Alt+.  - insert/cycle forward through last-argument history
  Ctrl+Alt+,  - cycle backward (undo an over-shot Ctrl+Alt+.)
  Alt+.       - unaffected, retains its historical panel fast-find behavior

Usage: python3 test_yank_last_arg.py /path/to/far2l
Requires: pyte (pip install pyte --break-system-packages)
"""
import os, pty, select, time, fcntl, termios, struct, sys, shutil, pwd

import pyte

FAR2L = sys.argv[1] if len(sys.argv) > 1 else 'far2l'
ROWS, COLS = 30, 100
CMDLINE_ROW = ROWS - 2

# kitty keyboard protocol sequences: <unicode-codepoint>;<modifiers>u
# modifiers bitmask: shift=1, alt=2, ctrl=4 (encoded value = bitmask+1)
CTRL_ALT_DOT = b'\x1b[46;7u'     # '.' (46) + ctrl+alt (4+2+1=7)
CTRL_ALT_COMMA = b'\x1b[44;7u'   # ',' (44) + ctrl+alt
ALT_DOT = b'\x1b.'               # legacy encoding: ESC + '.'

results = []


def setup_profile():
    real_home = pwd.getpwuid(os.geteuid()).pw_dir
    cfg = real_home + '/.config/far2l'
    shutil.rmtree(cfg, ignore_errors=True)
    os.makedirs(cfg + '/settings')
    # empty section: profile exists (no first-start dialogs), option keeps default
    with open(cfg + '/settings/config.ini', 'w') as f:
        f.write('[Cmdline]\n')


def start_far2l():
    screen = pyte.Screen(COLS, ROWS)
    stream = pyte.ByteStream(screen)
    pid, fd = pty.fork()
    if pid == 0:
        os.environ['TERM'] = 'xterm-256color'
        os.execv(FAR2L, ['far2l', '--tty'])
    fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', ROWS, COLS, 0, 0))
    return pid, fd, screen, stream


def feed(fd, stream, screen, timeout=2.0):
    end = time.time() + timeout
    while time.time() < end:
        r, _, _ = select.select([fd], [], [], 0.3)
        if r:
            try:
                d = os.read(fd, 65536)
            except OSError:
                return
            if not d:
                return
            stream.feed(d)


def cmdline(screen):
    return screen.display[CMDLINE_ROW].rstrip()


def check(name, cond, detail):
    results.append(cond)
    print(('OK  ' if cond else 'FAIL') + ' ' + name + ' -> ' + detail)


def stop(pid, fd):
    try:
        os.kill(pid, 9)
    except ProcessLookupError:
        pass
    try:
        os.close(fd)
    except OSError:
        pass


def wait_for_panels(fd, stream, screen):
    feed(fd, stream, screen, 4.0)
    for _ in range(10):
        keybar = screen.display[ROWS - 1]
        if '2UserMn' in keybar:   # real panel keybar; the About/Getting-Started
            return True           # dialog's keybar has this slot blank
        if any('Input instance index' in l for l in screen.display):
            os.write(fd, b'\r')
        # closes a possible first-run "About/Getting Started" dialog
        os.write(fd, b'\x1b')
        feed(fd, stream, screen, 1.5)
    return False


setup_profile()
pid, fd, screen, stream = start_far2l()
if not wait_for_panels(fd, stream, screen):
    print('FAIL: panels did not appear')
    stop(pid, fd)
    sys.exit(1)

for cmd in (b'echo one\r', b'echo two\r', b'echo three\r'):
    os.write(fd, cmd)
    feed(fd, stream, screen, 2.0)  # slow down to avoid a history-write race

os.write(fd, b'cp ')
feed(fd, stream, screen, 0.8)

os.write(fd, CTRL_ALT_DOT)
feed(fd, stream, screen, 0.8)
check('1x forward inserts most recent arg', 'cp three' in cmdline(screen), cmdline(screen))

os.write(fd, CTRL_ALT_DOT)
feed(fd, stream, screen, 0.8)
check('2x forward cycles to older arg', 'cp two' in cmdline(screen), cmdline(screen))

os.write(fd, CTRL_ALT_DOT)
feed(fd, stream, screen, 0.8)
check('3x forward cycles to oldest arg', 'cp one' in cmdline(screen), cmdline(screen))

os.write(fd, CTRL_ALT_COMMA)
feed(fd, stream, screen, 0.8)
check('1x backward returns to previous arg', 'cp two' in cmdline(screen), cmdline(screen))

os.write(fd, CTRL_ALT_COMMA)
feed(fd, stream, screen, 0.8)
check('2x backward returns to most recent arg', 'cp three' in cmdline(screen), cmdline(screen))

os.write(fd, CTRL_ALT_COMMA)
feed(fd, stream, screen, 0.8)
check('3x backward at newest entry is a no-op', 'cp three' in cmdline(screen), cmdline(screen))

os.write(fd, b'\x1b')
feed(fd, stream, screen, 1.0)

# Alt+. must retain its historical panel fast-find behavior, unaffected by
# the feature, even with a non-empty command line (this was the concern
# raised in code review that led to moving the feature off Alt+.)
os.write(fd, b'cp ')
feed(fd, stream, screen, 0.8)
os.write(fd, ALT_DOT)
feed(fd, stream, screen, 1.2)
fastfind_open = any(('Search' in l) or ('Поиск' in l) for l in screen.display)
check('Alt+. still opens panel fast-find (non-empty line)',
      fastfind_open and 'cp three' not in cmdline(screen), cmdline(screen))

stop(pid, fd)
print('---')
sys.exit(0 if all(results) else 1)

Запуск: python3 test_yank_last_arg.py /path/to/far2l. У меня и на моей машине, и на билде из этой ветки — все проверки проходят стабильно (несколько повторов подряд).

@simwin

simwin commented Sep 6, 2026

Copy link
Copy Markdown
Author

По CI: прогон check build от 6 августа истёк по таймауту одобрения (30 дней в очереди, ни одна джоба не запускалась) — https://github.com/elfmz/far2l/actions/runs/31129509315

С тех пор ветка обновилась, так что сборка на актуальном коде ещё не проверялась. Запустите, пожалуйста, workflow, когда дойдут руки до PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants