diff --git a/meson.build b/meson.build index 4ba5e590..7f83815a 100644 --- a/meson.build +++ b/meson.build @@ -422,7 +422,6 @@ ncmpc = executable('ncmpc', 'src/CustomColors.cxx', 'src/Styles.cxx', 'src/charset.cxx', - 'src/wreadln.cxx', 'src/Completion.cxx', 'src/strfsong.cxx', 'src/time_format.cxx', diff --git a/src/ChatPage.cxx b/src/ChatPage.cxx index 82e09739..fb142b89 100644 --- a/src/ChatPage.cxx +++ b/src/ChatPage.cxx @@ -4,7 +4,6 @@ #include "ChatPage.hxx" #include "PageMeta.hxx" #include "screen.hxx" -#include "screen_utils.hxx" #include "screen_status.hxx" #include "mpdclient.hxx" #include "i18n.h" @@ -12,6 +11,7 @@ #include "Command.hxx" #include "Options.hxx" #include "page/TextPage.hxx" +#include "dialogs/TextInputDialog.hxx" #include "util/StringAPI.hxx" #include @@ -150,8 +150,9 @@ ChatPage::SendMessage(struct mpdclient &c, const char *msg) noexcept inline Co::InvokeTask ChatPage::EnterMessage(struct mpdclient &c) { - auto message = screen_readln(screen, _("Your message"), - nullptr, nullptr, nullptr); + const auto message = co_await TextInputDialog{ + screen, _("Your message"), + }; /* the user entered an empty line */ if (message.empty()) diff --git a/src/OutputsPage.cxx b/src/OutputsPage.cxx index bf74be53..da53073b 100644 --- a/src/OutputsPage.cxx +++ b/src/OutputsPage.cxx @@ -7,10 +7,10 @@ #include "screen.hxx" #include "screen_status.hxx" #include "Command.hxx" -#include "screen_utils.hxx" #include "i18n.h" #include "mpdclient.hxx" #include "page/ListPage.hxx" +#include "dialogs/TextInputDialog.hxx" #include "ui/ListRenderer.hxx" #include "ui/paint.hxx" #include "util/FNVHash.hxx" @@ -167,7 +167,9 @@ OutputsPage::CreateNewPartition(struct mpdclient &c) noexcept if (!c.IsConnected()) co_return; - auto name = screen_readln(screen, _("Name"), nullptr, nullptr, nullptr); + const auto name = co_await TextInputDialog{ + screen, _("Name"), + }; if (name.empty()) co_return; diff --git a/src/QueuePage.cxx b/src/QueuePage.cxx index 4789f498..ccd23492 100644 --- a/src/QueuePage.cxx +++ b/src/QueuePage.cxx @@ -20,6 +20,7 @@ #include "screen_utils.hxx" #include "db_completion.hxx" #include "page/ListPage.hxx" +#include "dialogs/TextInputDialog.hxx" #include "ui/ListRenderer.hxx" #include "ui/ListText.hxx" #include "event/CoarseTimerEvent.hxx" @@ -300,10 +301,13 @@ handle_add_to_playlist(ScreenManager &screen, struct mpdclient &c) #endif /* get path */ - auto path = screen_readln(screen, _("Add"), - nullptr, - nullptr, - completion); + const auto path = co_await TextInputDialog{ + screen, + _("Add"), + {}, + nullptr, + completion, + }; /* add the path to the playlist */ if (!path.empty()) { diff --git a/src/SearchPage.cxx b/src/SearchPage.cxx index 0f7c99d5..5ece4233 100644 --- a/src/SearchPage.cxx +++ b/src/SearchPage.cxx @@ -11,9 +11,9 @@ #include "GlobalBindings.hxx" #include "charset.hxx" #include "mpdclient.hxx" -#include "screen_utils.hxx" #include "FileListPage.hxx" #include "filelist.hxx" +#include "dialogs/TextInputDialog.hxx" #include "ui/TextListRenderer.hxx" #include "lib/fmt/ToSpan.hxx" #include "util/StringAPI.hxx" @@ -414,10 +414,11 @@ SearchPage::Start(struct mpdclient &c) Clear(true); - pattern = screen_readln(screen, _("Search"), - nullptr, - &search_history, - nullptr); + pattern = co_await TextInputDialog{ + screen, _("Search"), + {}, + &search_history, + }; if (pattern.empty()) { lw.Reset(); diff --git a/src/dialogs/TextInputDialog.cxx b/src/dialogs/TextInputDialog.cxx new file mode 100644 index 00000000..5eb53195 --- /dev/null +++ b/src/dialogs/TextInputDialog.cxx @@ -0,0 +1,375 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#include "TextInputDialog.hxx" +#include "ui/Bell.hxx" +#include "ui/Keys.hxx" +#include "ui/Options.hxx" +#include "ui/Window.hxx" +#include "util/LocaleString.hxx" +#include "Completion.hxx" +#include "Styles.hxx" + +#ifndef _WIN32 +#include "WaitUserInput.hxx" +#endif + +#include + +#include + +using std::string_view_literals::operator""sv; + +/** max items stored in the history list */ +static constexpr std::size_t wrln_max_history_length = 32; + +inline void +TextInputDialog::SetReady() noexcept +{ + assert(!ready); + ready = true; + + /* update history */ + if (history) { + if (!value.empty()) { + /* update the current history entry */ + *hcurrent = value; + } else { + /* the line was empty - remove the current history entry */ + history->erase(hcurrent); + } + + auto history_length = history->size(); + while (history_length > wrln_max_history_length) { + history->pop_front(); + --history_length; + } + } + + if (continuation) + continuation.resume(); +} + +/** converts a byte position to a screen column */ +[[gnu::pure]] +static unsigned +byte_to_screen(const char *data, size_t x) noexcept +{ +#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) + assert(x <= strlen(data)); + + return StringWidthMB({data, x}); +#else + (void)data; + + return (unsigned)x; +#endif +} + +/** finds the first character which doesn't fit on the screen */ +[[gnu::pure]] +static size_t +screen_to_bytes(const char *data, unsigned width) noexcept +{ +#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) + size_t length = strlen(data); + + while (true) { + unsigned p_width = StringWidthMB({data, length}); + if (p_width <= width) + return length; + + --length; + } +#else + (void)data; + + return (size_t)width; +#endif +} + +inline unsigned +TextInputDialog::GetCursorColumn() const noexcept +{ + return byte_to_screen(value.data() + start, cursor - start); +} + +/** returns the offset in the string to align it at the right border + of the screen */ +[[gnu::pure]] +static inline size_t +right_align_bytes(const char *data, size_t right, unsigned width) noexcept +{ +#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) + size_t start = 0; + + assert(right <= strlen(data)); + + while (start < right) { + if (StringWidthMB({data + start, right - start}) < width) + break; + + start += CharSizeMB({data + start, right - start}); + } + + return start; +#else + (void)data; + + return right >= width ? right + 1 - width : 0; +#endif +} + +inline void +TextInputDialog::MoveCursorRight() noexcept +{ + if (cursor == value.length()) + return; + + size_t size = CharSizeMB(value.substr(cursor)); + cursor += size; + if (GetCursorColumn() >= width) + start = right_align_bytes(value.c_str(), cursor, width); +} + +inline void +TextInputDialog::MoveCursorLeft() noexcept +{ + const char *v = value.c_str(); + const char *new_cursor = PrevCharMB(v, v + cursor); + cursor = new_cursor - v; + if (cursor < start) + start = cursor; +} + +inline void +TextInputDialog::MoveCursorToEnd() noexcept +{ + cursor = value.length(); + if (GetCursorColumn() >= width) + start = right_align_bytes(value.c_str(), + cursor, width); +} + +inline void +TextInputDialog::InsertByte([[maybe_unused]] const Window window, int key) noexcept +{ + size_t length = 1; +#if (defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)) && !defined(_WIN32) + char buffer[32] = { (char)key }; + WaitUserInput wui; + + /* wide version: try to complete the multibyte sequence */ + + while (length < sizeof(buffer)) { + if (!IsIncompleteCharMB({buffer, length})) + /* sequence is complete */ + break; + + /* poll for more bytes on stdin, without timeout */ + + if (!wui.IsReady()) + /* no more input from keyboard */ + break; + + buffer[length++] = window.GetChar(); + } + + value.insert(cursor, buffer, length); + +#else + value.insert(cursor, key); +#endif + + cursor += length; + if (GetCursorColumn() >= width) + start = right_align_bytes(value.c_str(), cursor, width); +} + +inline void +TextInputDialog::DeleteChar(size_t x) noexcept +{ + assert(x < value.length()); + + size_t length = CharSizeMB(value.substr(x)); + value.erase(x, length); +} + +void +TextInputDialog::OnLeave(const Window window) noexcept +{ + curs_set(0); + + if (ui_options.enable_colors) + window.SetBackgroundStyle(Style::STATUS); +} + +void +TextInputDialog::OnCancel() noexcept +{ + value.clear(); + SetReady(); +} + +bool +TextInputDialog::OnKey(const Window window, int key) +{ + if (key == KEY_RETURN || key == KEY_LINEFEED) { + SetReady(); + return true; + } + + /* check if key is a function key */ + for (size_t i = 0; i < 63; i++) + if (key == (int)KEY_F(i)) { + key = KEY_F(1); + i = 64; + } + + switch (key) { + case KEY_TAB: +#ifndef NCMPC_MINI + if (completion != nullptr) { + completion->Pre(value); + auto r = completion->Complete(value); + if (!r.new_prefix.empty()) { + value = std::move(r.new_prefix); + MoveCursorToEnd(); + } else + Bell(); + + completion->Post(value, r.range); + } +#endif + break; + + case KEY_CTL('C'): + case KEY_CTL('G'): + Bell(); + if (history) { + history->pop_back(); + } + Cancel(); + return true; + + case KEY_LEFT: + case KEY_CTL('B'): + MoveCursorLeft(); + break; + case KEY_RIGHT: + case KEY_CTL('F'): + MoveCursorRight(); + break; + case KEY_HOME: + case KEY_CTL('A'): + cursor = 0; + start = 0; + break; + case KEY_END: + case KEY_CTL('E'): + MoveCursorToEnd(); + break; + case KEY_CTL('K'): + value.erase(cursor); + break; + case KEY_CTL('U'): + value.erase(0, cursor); + cursor = 0; + break; + case KEY_CTL('W'): + /* Firstly remove trailing spaces. */ + for (; cursor > 0 && value[cursor - 1] == ' ';) + { + MoveCursorLeft(); + DeleteChar(); + } + /* Then remove word until next space. */ + for (; cursor > 0 && value[cursor - 1] != ' ';) + { + MoveCursorLeft(); + DeleteChar(); + } + break; + case KEY_BACKSPACE3: + case KEY_BACKSPACE2: /* handle backspace: copy all */ + case KEY_BACKSPACE: /* chars starting from curpos */ + if (cursor > 0) { /* - 1 from buf[n+1] to buf */ + MoveCursorLeft(); + DeleteChar(); + } + break; + case KEY_DC: /* handle delete key. As above */ + case KEY_CTL('D'): + if (cursor < value.length()) + DeleteChar(); + break; + case KEY_UP: + case KEY_CTL('P'): + /* get previous history entry */ + if (history && hlist != history->begin()) { + if (hlist == hcurrent) + /* save the current line */ + *hlist = value; + + /* get previous line */ + --hlist; + value = *hlist; + } + MoveCursorToEnd(); + break; + case KEY_DOWN: + case KEY_CTL('N'): + /* get next history entry */ + if (history && std::next(hlist) != history->end()) { + /* get next line */ + ++hlist; + value = *hlist; + } + MoveCursorToEnd(); + break; + + case KEY_IC: + case KEY_PPAGE: + case KEY_NPAGE: + case KEY_F(1): + /* ignore char */ + break; + default: + if (key >= 32) + InsertByte(window, key); + } + + return true; +} + +void +TextInputDialog::Paint(const Window window) const noexcept +{ + if (ui_options.enable_colors) + window.SetBackgroundStyle(Style::INPUT); + + SelectStyle(window, Style::STATUS_ALERT); + window.String({0, 0}, prompt); + window.String(": "sv); + + point = window.GetCursor(); + width = window.GetWidth() - point.x; + + SelectStyle(window, Style::INPUT); + + /* print visible part of the line buffer */ + if (masked) { + const unsigned value_width = StringWidthMB(value.substr(start)); + window.HLine(value_width, '*'); + window.MoveCursor(point + Size{value_width, 0}); + } else { + window.String({value.c_str() + start, screen_to_bytes(value.c_str() + start, width)}); + } + + /* clear the rest */ + window.ClearToEol(); + /* move the cursor to the correct position */ + window.MoveCursor({point.x + (int)GetCursorColumn(), point.y}); + + curs_set(1); +} diff --git a/src/dialogs/TextInputDialog.hxx b/src/dialogs/TextInputDialog.hxx new file mode 100644 index 00000000..cc63e2a9 --- /dev/null +++ b/src/dialogs/TextInputDialog.hxx @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#pragma once + +#include "ModalDialog.hxx" +#include "co/AwaitableHelper.hxx" +#include "ui/Point.hxx" +#include "History.hxx" + +#include + +class Completion; + +/** + * A #ModalDialog that asks the user to input text. + * + * This dialog is supposed to be awaited from a coroutine using + * co_await. It suspends the caller while waiting for user input. + */ +class TextInputDialog final : public ModalDialog { + const std::string_view prompt; + + /** the current value */ + std::string value; + + History *const history; + Completion *const completion; + + History::iterator hlist, hcurrent; + + /** the origin coordinates in the window */ + mutable Point point; + + /** the screen width of the input field */ + mutable unsigned width; + + /** is the input masked, i.e. characters displayed as '*'? */ + const bool masked; + + bool ready = false; + + /** the byte position of the cursor */ + std::size_t cursor = 0; + + /** the byte position displayed at the origin (for horizontal + scrolling) */ + std::size_t start = 0; + + std::coroutine_handle<> continuation; + + using Awaitable = Co::AwaitableHelper; + friend Awaitable; + +public: + /** + * @param _prompt the human-readable prompt to be displayed + * (including question mark if desired); the pointed-by memory + * is owned by the caller and must remain valid during the + * lifetime of this dialog + * + * @param _value the initial value + * + * @param _masked do not display the text, show asterisks + * instead (for password entry) + */ + TextInputDialog(ModalDock &_dock, + std::string_view _prompt, + std::string &&_value={}, + History *_history=nullptr, + Completion *_completion=nullptr, + bool _masked=false) noexcept + :ModalDialog(_dock), prompt(_prompt), + value(std::move(_value)), + history(_history), completion(_completion), + masked(_masked) + { + Show(); + + if (history) { + /* append the a new line to our history list */ + history->emplace_back(); + /* hlist points to the current item in the history list */ + hcurrent = hlist = std::prev(history->end()); + } + } + + ~TextInputDialog() noexcept { + Hide(); + } + + /** + * Await completion of this dialog. + * + * @return a std::string; if canceled by the user, returns an + * empty string + */ + Awaitable operator co_await() noexcept { + return *this; + } + +private: + bool IsReady() const noexcept { + return ready; + } + + std::string TakeValue() noexcept { + return std::move(value); + } + + void SetReady() noexcept; + + /** returns the screen column where the cursor is located */ + [[gnu::pure]] + unsigned GetCursorColumn() const noexcept; + + /** move the cursor one step to the right */ + void MoveCursorRight() noexcept; + + /** move the cursor one step to the left */ + void MoveCursorLeft() noexcept; + + /** move the cursor to the end of the line */ + void MoveCursorToEnd() noexcept; + + void InsertByte(Window window, int key) noexcept; + void DeleteChar(size_t x) noexcept; + void DeleteChar() noexcept { + DeleteChar(cursor); + } + +public: + /* virtual methodds from Modal */ + void OnLeave(Window window) noexcept override; + void OnCancel() noexcept override; + bool OnKey(Window window, int key) override; + void Paint(Window window) const noexcept override; +}; diff --git a/src/dialogs/meson.build b/src/dialogs/meson.build index 12358a4d..61a6e461 100644 --- a/src/dialogs/meson.build +++ b/src/dialogs/meson.build @@ -2,6 +2,7 @@ dialogs = static_library( 'dialogs', 'ModalDialog.cxx', 'YesNoDialog.cxx', + 'TextInputDialog.cxx', include_directories: inc, dependencies: [ ui_dep, diff --git a/src/page/FindSupport.cxx b/src/page/FindSupport.cxx index 907ae793..04523530 100644 --- a/src/page/FindSupport.cxx +++ b/src/page/FindSupport.cxx @@ -2,12 +2,14 @@ // Copyright The Music Player Daemon Project #include "FindSupport.hxx" +#include "screen.hxx" #include "screen_utils.hxx" #include "screen_status.hxx" #include "screen.hxx" #include "AsyncUserInput.hxx" #include "i18n.h" #include "Command.hxx" +#include "dialogs/TextInputDialog.hxx" #include "ui/Bell.hxx" #include "ui/ListWindow.hxx" #include "ui/Options.hxx" @@ -25,13 +27,17 @@ FindSupport::DoFind(ListWindow &lw, const ListText &text, bool reversed) noexcep { if (last.empty()) { const char *const prompt = reversed ? RFIND_PROMPT : FIND_PROMPT; - char *value = ui_options.find_show_last_pattern - ? (char *) -1 : nullptr; - last = screen_readln(screen, - prompt, - value, - &history, - nullptr); + + std::string value; + if (ui_options.find_show_last_pattern && !history.empty()) + value = history.back(); + + last = co_await TextInputDialog{ + screen, + prompt, + std::move(value), + &history, + }; } if (last.empty()) diff --git a/src/save_playlist.cxx b/src/save_playlist.cxx index 8bee80c7..c126a0a5 100644 --- a/src/save_playlist.cxx +++ b/src/save_playlist.cxx @@ -11,6 +11,7 @@ #include "Completion.hxx" #include "screen.hxx" #include "screen_utils.hxx" +#include "dialogs/TextInputDialog.hxx" #include "dialogs/YesNoDialog.hxx" #include "co/InvokeTask.hxx" @@ -69,10 +70,13 @@ playlist_save(ScreenManager &screen, struct mpdclient &c, #endif /* query the user for a filename */ - filename = screen_readln(screen, _("Save queue as"), - nullptr, - nullptr, - completion); + filename = co_await TextInputDialog{ + screen, _("Save queue as"), + {}, + nullptr, + completion, + }; + if (filename.empty()) co_return; } diff --git a/src/screen.cxx b/src/screen.cxx index c112499c..bbdb062b 100644 --- a/src/screen.cxx +++ b/src/screen.cxx @@ -5,7 +5,6 @@ #include "PageMeta.hxx" #include "screen_list.hxx" #include "screen_status.hxx" -#include "screen_utils.hxx" #include "Command.hxx" #include "config.h" #include "i18n.h" @@ -19,6 +18,7 @@ #include "QueuePage.hxx" #include "page/Page.hxx" #include "dialogs/ModalDialog.hxx" +#include "dialogs/TextInputDialog.hxx" #include "ui/Options.hxx" #include "co/Task.hxx" #include "util/ScopeExit.hxx" @@ -342,7 +342,10 @@ EnterPassword(ScreenManager &screen, struct mpdclient &c) c.authenticating = false; }; - co_return screen_read_password(screen, nullptr); + co_return co_await TextInputDialog{ + screen, _("Password"), {}, + nullptr, nullptr, true, + }; } inline Co::InvokeTask diff --git a/src/screen_utils.cxx b/src/screen_utils.cxx index cee33bec..294803ea 100644 --- a/src/screen_utils.cxx +++ b/src/screen_utils.cxx @@ -3,10 +3,7 @@ #include "screen_utils.hxx" #include "screen.hxx" -#include "config.h" -#include "i18n.h" #include "Styles.hxx" -#include "wreadln.hxx" #include "ui/Options.hxx" #include "util/ScopeExit.hxx" #include "config.h" @@ -75,62 +72,6 @@ screen_getch(ScreenManager &screen, const char *prompt) noexcept return key; } -std::string -screen_readln(ScreenManager &screen, const char *prompt, - const char *value, - History *history, - Completion *completion) noexcept -{ - const auto &window = screen.status_bar.GetWindow(); - - if (ui_options.enable_colors) - window.SetBackgroundStyle(Style::INPUT); - - AtScopeExit(&window) { - if (ui_options.enable_colors) - window.SetBackgroundStyle(Style::STATUS); - }; - - window.MoveCursor({0, 0}); - - if (prompt != nullptr) { - SelectStyle(window, Style::STATUS_ALERT); - window.String(prompt); - window.String(": "sv); - } - - SelectStyle(window, Style::INPUT); - - return wreadln(window, value, history, completion); -} - -std::string -screen_read_password(ScreenManager &screen, const char *prompt) noexcept -{ - const auto &window = screen.status_bar.GetWindow(); - - if (ui_options.enable_colors) - window.SetBackgroundStyle(Style::INPUT); - - AtScopeExit(&window) { - if (ui_options.enable_colors) - window.SetBackgroundStyle(Style::STATUS); - }; - - window.MoveCursor({0, 0}); - SelectStyle(window, Style::STATUS_ALERT); - - if (prompt == nullptr) - prompt = _("Password"); - - window.String(prompt); - window.String(": "sv); - - SelectStyle(window, Style::INPUT); - - return wreadln_masked(window, nullptr); -} - static const char * CompletionDisplayString(const char *value) noexcept { diff --git a/src/screen_utils.hxx b/src/screen_utils.hxx index 64aaab4c..22a3ff80 100644 --- a/src/screen_utils.hxx +++ b/src/screen_utils.hxx @@ -4,7 +4,6 @@ #ifndef SCREEN_UTILS_H #define SCREEN_UTILS_H -#include "History.hxx" #include "Completion.hxx" class ScreenManager; @@ -13,14 +12,6 @@ class ScreenManager; int screen_getch(ScreenManager &screen, const char *prompt) noexcept; -std::string -screen_read_password(ScreenManager &screen, const char *prompt) noexcept; - -std::string -screen_readln(ScreenManager &screen, const char *prompt, - const char *value, - History *history, Completion *completion) noexcept; - void screen_display_completion_list(ScreenManager &screen, Completion::Range range) noexcept; diff --git a/src/wreadln.cxx b/src/wreadln.cxx deleted file mode 100644 index 9033888f..00000000 --- a/src/wreadln.cxx +++ /dev/null @@ -1,466 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "wreadln.hxx" -#include "Completion.hxx" -#include "config.h" -#include "ui/Bell.hxx" -#include "ui/Keys.hxx" -#include "ui/Point.hxx" -#include "ui/Window.hxx" -#include "util/LocaleString.hxx" -#include "util/ScopeExit.hxx" - -#include - -#include -#include -#include - -#ifndef _WIN32 -#include "WaitUserInput.hxx" -#include -#endif - -struct wreadln { - /** the ncurses window where this field is displayed */ - const Window window; - - /** the origin coordinates in the window */ - Point point; - - /** the screen width of the input field */ - unsigned width; - - /** is the input masked, i.e. characters displayed as '*'? */ - const bool masked; - - /** the byte position of the cursor */ - size_t cursor = 0; - - /** the byte position displayed at the origin (for horizontal - scrolling) */ - size_t start = 0; - - /** the current value */ - std::string value; - - wreadln(const Window _window, bool _masked) noexcept - :window(_window), masked(_masked) {} - - /** draw line buffer and update cursor position */ - void Paint() const noexcept; - - /** returns the screen column where the cursor is located */ - [[gnu::pure]] - unsigned GetCursorColumn() const noexcept; - - /** move the cursor one step to the right */ - void MoveCursorRight() noexcept; - - /** move the cursor one step to the left */ - void MoveCursorLeft() noexcept; - - /** move the cursor to the end of the line */ - void MoveCursorToEnd() noexcept; - - void InsertByte(int key) noexcept; - void DeleteChar(size_t x) noexcept; - void DeleteChar() noexcept { - DeleteChar(cursor); - } -}; - -/** max items stored in the history list */ -static constexpr std::size_t wrln_max_history_length = 32; - -/** converts a byte position to a screen column */ -[[gnu::pure]] -static unsigned -byte_to_screen(const char *data, size_t x) noexcept -{ -#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) - assert(x <= strlen(data)); - - return StringWidthMB({data, x}); -#else - (void)data; - - return (unsigned)x; -#endif -} - -/** finds the first character which doesn't fit on the screen */ -[[gnu::pure]] -static size_t -screen_to_bytes(const char *data, unsigned width) noexcept -{ -#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) - size_t length = strlen(data); - - while (true) { - unsigned p_width = StringWidthMB({data, length}); - if (p_width <= width) - return length; - - --length; - } -#else - (void)data; - - return (size_t)width; -#endif -} - -unsigned -wreadln::GetCursorColumn() const noexcept -{ - return byte_to_screen(value.data() + start, cursor - start); -} - -/** returns the offset in the string to align it at the right border - of the screen */ -[[gnu::pure]] -static inline size_t -right_align_bytes(const char *data, size_t right, unsigned width) noexcept -{ -#if defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE) - size_t start = 0; - - assert(right <= strlen(data)); - - while (start < right) { - if (StringWidthMB({data + start, right - start}) < width) - break; - - start += CharSizeMB({data + start, right - start}); - } - - return start; -#else - (void)data; - - return right >= width ? right + 1 - width : 0; -#endif -} - -void -wreadln::MoveCursorRight() noexcept -{ - if (cursor == value.length()) - return; - - size_t size = CharSizeMB(value.substr(cursor)); - cursor += size; - if (GetCursorColumn() >= width) - start = right_align_bytes(value.c_str(), cursor, width); -} - -void -wreadln::MoveCursorLeft() noexcept -{ - const char *v = value.c_str(); - const char *new_cursor = PrevCharMB(v, v + cursor); - cursor = new_cursor - v; - if (cursor < start) - start = cursor; -} - -void -wreadln::MoveCursorToEnd() noexcept -{ - cursor = value.length(); - if (GetCursorColumn() >= width) - start = right_align_bytes(value.c_str(), - cursor, width); -} - -void -wreadln::Paint() const noexcept -{ - window.MoveCursor(point); - /* print visible part of the line buffer */ - if (masked) - window.HLine(StringWidthMB(value.substr(start)), '*'); - else - window.String({value.c_str() + start, screen_to_bytes(value.c_str() + start, width)}); - /* clear the rest */ - window.ClearToEol(); - /* move the cursor to the correct position */ - window.MoveCursor({point.x + (int)GetCursorColumn(), point.y}); - /* tell ncurses to redraw the screen */ - doupdate(); -} - -void -wreadln::InsertByte(int key) noexcept -{ - size_t length = 1; -#if (defined(HAVE_CURSES_ENHANCED) || defined(ENABLE_MULTIBYTE)) && !defined(_WIN32) - char buffer[32] = { (char)key }; - WaitUserInput wui; - - /* wide version: try to complete the multibyte sequence */ - - while (length < sizeof(buffer)) { - if (!IsIncompleteCharMB({buffer, length})) - /* sequence is complete */ - break; - - /* poll for more bytes on stdin, without timeout */ - - if (!wui.IsReady()) - /* no more input from keyboard */ - break; - - buffer[length++] = window.GetChar(); - } - - value.insert(cursor, buffer, length); - -#else - value.insert(cursor, key); -#endif - - cursor += length; - if (GetCursorColumn() >= width) - start = right_align_bytes(value.c_str(), cursor, width); -} - -void -wreadln::DeleteChar(size_t x) noexcept -{ - assert(x < value.length()); - - size_t length = CharSizeMB(value.substr(x)); - value.erase(x, length); -} - -/* libcurses version */ - -static std::string -_wreadln(const Window window, - const char *initial_value, - History *history, - Completion *completion, - bool masked) noexcept -{ - struct wreadln wr{window, masked}; - History::iterator hlist, hcurrent; - -#ifdef NCMPC_MINI - (void)completion; -#endif - - /* make sure the cursor is visible */ - curs_set(1); - AtScopeExit() { curs_set(0); }; - /* retrieve y and x0 position */ - wr.point = window.GetCursor(); - wr.width = window.GetWidth() - wr.point.x; - - if (history) { - /* append the a new line to our history list */ - history->emplace_back(); - /* hlist points to the current item in the history list */ - hcurrent = hlist = std::prev(history->end()); - } - - if (initial_value == (char *)-1) { - /* get previous history entry */ - if (history && hlist != history->begin()) { - /* get previous line */ - --hlist; - wr.value = *hlist; - } - } else if (initial_value) { - /* copy the initial value to the line buffer */ - wr.value = initial_value; - } - - wr.MoveCursorToEnd(); - wr.Paint(); - -#ifndef _WIN32 - WaitUserInput wui; -#endif - - int key = 0; - while (key != 13 && key != '\n') { - key = window.GetChar(); - -#ifndef _WIN32 - if (key == ERR && errno == EAGAIN) { - if (wui.Wait()) - continue; - else - break; - } -#endif - - /* check if key is a function key */ - for (size_t i = 0; i < 63; i++) - if (key == (int)KEY_F(i)) { - key = KEY_F(1); - i = 64; - } - - switch (key) { -#ifdef HAVE_GETMOUSE - case KEY_MOUSE: /* ignore mouse events */ -#endif - case ERR: /* ignore errors */ - break; - - case KEY_TAB: -#ifndef NCMPC_MINI - if (completion != nullptr) { - completion->Pre(wr.value); - auto r = completion->Complete(wr.value); - if (!r.new_prefix.empty()) { - wr.value = std::move(r.new_prefix); - wr.MoveCursorToEnd(); - } else - Bell(); - - completion->Post(wr.value, r.range); - } -#endif - break; - - case KEY_CTL('C'): - case KEY_CTL('G'): - Bell(); - if (history) { - history->pop_back(); - } - return {}; - - case KEY_LEFT: - case KEY_CTL('B'): - wr.MoveCursorLeft(); - break; - case KEY_RIGHT: - case KEY_CTL('F'): - wr.MoveCursorRight(); - break; - case KEY_HOME: - case KEY_CTL('A'): - wr.cursor = 0; - wr.start = 0; - break; - case KEY_END: - case KEY_CTL('E'): - wr.MoveCursorToEnd(); - break; - case KEY_CTL('K'): - wr.value.erase(wr.cursor); - break; - case KEY_CTL('U'): - wr.value.erase(0, wr.cursor); - wr.cursor = 0; - break; - case KEY_CTL('W'): - /* Firstly remove trailing spaces. */ - for (; wr.cursor > 0 && wr.value[wr.cursor - 1] == ' ';) - { - wr.MoveCursorLeft(); - wr.DeleteChar(); - } - /* Then remove word until next space. */ - for (; wr.cursor > 0 && wr.value[wr.cursor - 1] != ' ';) - { - wr.MoveCursorLeft(); - wr.DeleteChar(); - } - break; - case KEY_BACKSPACE3: - case KEY_BACKSPACE2: /* handle backspace: copy all */ - case KEY_BACKSPACE: /* chars starting from curpos */ - if (wr.cursor > 0) { /* - 1 from buf[n+1] to buf */ - wr.MoveCursorLeft(); - wr.DeleteChar(); - } - break; - case KEY_DC: /* handle delete key. As above */ - case KEY_CTL('D'): - if (wr.cursor < wr.value.length()) - wr.DeleteChar(); - break; - case KEY_UP: - case KEY_CTL('P'): - /* get previous history entry */ - if (history && hlist != history->begin()) { - if (hlist == hcurrent) - /* save the current line */ - *hlist = wr.value; - - /* get previous line */ - --hlist; - wr.value = *hlist; - } - wr.MoveCursorToEnd(); - break; - case KEY_DOWN: - case KEY_CTL('N'): - /* get next history entry */ - if (history && std::next(hlist) != history->end()) { - /* get next line */ - ++hlist; - wr.value = *hlist; - } - wr.MoveCursorToEnd(); - break; - - case KEY_LINEFEED: - case KEY_RETURN: - case KEY_IC: - case KEY_PPAGE: - case KEY_NPAGE: - case KEY_F(1): - /* ignore char */ - break; - default: - if (key >= 32) - wr.InsertByte(key); - } - - wr.Paint(); - } - - /* update history */ - if (history) { - if (!wr.value.empty()) { - /* update the current history entry */ - *hcurrent = wr.value; - } else { - /* the line was empty - remove the current history entry */ - history->erase(hcurrent); - } - - auto history_length = history->size(); - while (history_length > wrln_max_history_length) { - history->pop_front(); - --history_length; - } - } - - return std::move(wr.value); -} - -std::string -wreadln(const Window window, - const char *initial_value, - History *history, - Completion *completion) noexcept -{ - return _wreadln(window, initial_value, - history, completion, false); -} - -std::string -wreadln_masked(const Window window, - const char *initial_value) noexcept -{ - return _wreadln(window, initial_value, nullptr, nullptr, true); -} diff --git a/src/wreadln.hxx b/src/wreadln.hxx deleted file mode 100644 index ffcb7725..00000000 --- a/src/wreadln.hxx +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#pragma once - -#include "History.hxx" - -#include - -class Completion; -struct Window; - -/** - * - * This function calls curs_set(1), to enable cursor. It will not - * restore this settings when exiting. - * - * @param the curses window to use - * @param initial_value initial value or nullptr for a empty line; - * (char *) -1 = get value from history - * @param history a pointer to a history list or nullptr - * @param a #Completion instance or nullptr - */ -std::string -wreadln(Window window, - const char *initial_value, - History *history, - Completion *completion) noexcept; - -std::string -wreadln_masked(Window window, - const char *initial_value) noexcept;