xaizek / pipedial (License: GPLv3 only) (since 2019-01-08)
One more tool for selecting something in console.
<root> / src / PipeDial.cpp (484002cd03ae1b68640bccedb8f4fac57169c475) (5,978B) (mode 100644) [raw]
// pipedial -- terminal element picker
// Copyright (C) 2019 xaizek <xaizek@posteo.net>
//
// This file is part of pipedial.
//
// pipedial is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// pipedial is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with pipedial.  If not, see <https://www.gnu.org/licenses/>.

#include "PipeDial.hpp"

#include <string>
#include <utility>
#include <vector>

#include "cursed/List.hpp"
#include "cursed/Prompt.hpp"
#include "cursed/Track.hpp"

#include "vle/KeyDispatcher.hpp"
#include "vle/Mode.hpp"
#include "vle/Modes.hpp"

#include "PromptRequest.hpp"

// Help message.
static const std::wstring helpText = LR"(
               SUMMARY OF PIPEDIAL SHORTCUTS
               =============================

{Escape} -- abort partially typed shortcut.

Normal Mode
-----------

q -- quit the application with no selection.

h -- enter Help mode.

{Enter} -- select current item and quit.

e -- edit current item and accept the result.

& -- start interactive filtering.

[count]gg -- put cursor on the first or [count]-th element.

[count]G -- put cursor on the last or [count]-th element.

[count]j -- move cursor [count] (1 by default) elements down.

[count]k -- move cursor [count] (1 by default) elements up.

Help Mode
---------

h -- return to Normal mode.)";

PipeDial::PipeDial(std::wstring titleText, std::vector<std::wstring> lines)
    : lines(lines), title(std::move(titleText)), help(helpText), quit(false)
{
    list.setItems(std::move(lines));

    track.addItem(&title);
    track.addItem(&list);
    track.addItem(&inputBuf);

    std::vector<vle::Mode> allModes;
    allModes.emplace_back(buildNormalMode());
    allModes.emplace_back(buildHelpMode());
    modes.setModes(std::move(allModes));
    modes.switchTo("normal");
}

vle::Mode
PipeDial::buildNormalMode()
{
    vle::Mode normalMode("normal");

    normalMode.setUsesCount(true);
    normalMode.addShortcut({ L"G", [&](int count) {
        if (count < 0) {
            list.moveToLast();
        } else {
            list.moveToPos(count);
        }
    }, "go to the last or [count]-th item" });
    normalMode.addShortcut({ L"gg", [&](int count) {
        if (count < 0) {
            list.moveToFirst();
        } else {
            list.moveToPos(count);
        }
    }, "go to the first or [count]-th item" });
    normalMode.addShortcut({ L"j", [&](int count) {
        list.moveDown(count < 0 ? 1 : count);
    }, "go to item below" });
    normalMode.addShortcut({ L"k", [&](int count) {
        list.moveUp(count < 0 ? 1 : count);
    }, "go to item above" });
    normalMode.addShortcut({ L"q", [&]() {
        quit = true;
    }, "quit the application with no selection" });
    normalMode.addShortcut({ L"\n", [&]() {
        quit = true;
        result = list.getCurrent();
    }, "select current item and quit" });
    normalMode.addShortcut({ L"h", [&]() {
        modes.switchTo("help");
        screen.setMainWidget(&help);
    }, "display help" });
    normalMode.addShortcut({ L"e", [&]() {
        cursed::Prompt prompt;
        cursed::Track tmpTrack;
        tmpTrack.addItem(&title);
        tmpTrack.addItem(&list);
        tmpTrack.addItem(&prompt);

        PromptRequest request(input, screen, prompt);
        screen.setMainWidget(&tmpTrack);
        if (PromptResult r = request.prompt(L"Edit and accept: ",
                                            list.getCurrent())) {
            result = r.getResult();
            quit = true;
        } else {
            screen.setMainWidget(&track);
        }
    }, "edit and accept" });
    normalMode.addShortcut({ L"&", [&]() {
        cursed::List filterList;
        filterList.setItems(lines);

        cursed::Prompt prompt;
        cursed::Track tmpTrack;
        tmpTrack.addItem(&title);
        tmpTrack.addItem(&filterList);
        tmpTrack.addItem(&prompt);

        std::vector<std::wstring> filtered = lines;
        auto update = [&](const std::wstring &filter) {
            filtered.clear();
            for (const std::wstring &line : lines) {
                if (line.find(filter) != std::wstring::npos) {
                    filtered.push_back(line);
                }
            }
            filterList.setItems(filtered);
        };

        PromptRequest request(input, screen, prompt, update);
        screen.setMainWidget(&tmpTrack);
        if (request.prompt(L"&/", L"")) {
            list.setItems(std::move(filtered));
        }
        screen.setMainWidget(&track);
    }, "start interactive filtering" });

    return normalMode;
}

vle::Mode
PipeDial::buildHelpMode()
{
    vle::Mode helpMode("help");

    helpMode.addShortcut({ L"h", [&]() {
        modes.switchTo("normal");
        screen.setMainWidget(&track);
    }, "display help" });
    helpMode.addShortcut({ L"j", [&]() {
        help.scrollDown();
    }, "go to item below" });
    helpMode.addShortcut({ L"k", [&]() {
        help.scrollUp();
    }, "go to item above" });

    return helpMode;
}

std::wstring
PipeDial::run()
{
    screen.setMainWidget(&track);
    screen.draw();

    vle::KeyDispatcher dispatcher;

    while (cursed::InputElement ie = input.read()) {
        if (ie.isTerminalResize()) {
            screen.resize();
            screen.draw();
            continue;
        }

        if (!ie.isFunctionalKey()) {
            dispatcher.dispatch(ie);
        }

        if (quit) {
            break;
        }

        inputBuf.setText(dispatcher.getPendingInput());
        screen.draw();
    }

    return result;
}
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/pipedial

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/pipedial

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master