diff options
Diffstat (limited to 'src/interface-curses')
-rw-r--r-- | src/interface-curses/Makefile.am | 3 | ||||
-rw-r--r-- | src/interface-curses/curses-icons.c | 398 | ||||
-rw-r--r-- | src/interface-curses/curses-icons.h | 28 | ||||
-rw-r--r-- | src/interface-curses/curses-info-popup.c | 23 | ||||
-rw-r--r-- | src/interface-curses/curses-info-popup.h | 2 | ||||
-rw-r--r-- | src/interface-curses/curses-utils.c | 72 | ||||
-rw-r--r-- | src/interface-curses/curses-utils.h | 17 | ||||
-rw-r--r-- | src/interface-curses/interface.c | 246 |
8 files changed, 674 insertions, 115 deletions
diff --git a/src/interface-curses/Makefile.am b/src/interface-curses/Makefile.am index 14fc920..44fb658 100644 --- a/src/interface-curses/Makefile.am +++ b/src/interface-curses/Makefile.am @@ -6,4 +6,5 @@ AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value noinst_LTLIBRARIES = libsciteco-interface.la libsciteco_interface_la_SOURCES = interface.c \ curses-utils.c curses-utils.h \ - curses-info-popup.c curses-info-popup.h + curses-info-popup.c curses-info-popup.h \ + curses-icons.c curses-icons.h diff --git a/src/interface-curses/curses-icons.c b/src/interface-curses/curses-icons.c new file mode 100644 index 0000000..1a1ba3a --- /dev/null +++ b/src/interface-curses/curses-icons.c @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2012-2024 Robin Haberkorn + * + * This program 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. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <stdlib.h> +#include <string.h> + +#include <glib.h> + +#include <curses.h> + +#include "sciteco.h" +#include "curses-icons.h" + +typedef struct { + const gchar *name; + gunichar c; +} teco_curses_icon_t; + +/* + * The following icons have initially been adapted from exa, + * but icons have since been added and removed. + * + * They require fonts with additional symbols, eg. + * Nerd Fonts (https://www.nerdfonts.com/). + * + * They MUST be kept presorted, so we can perform binary searches. + */ + +/** Mapping of complete filenames to Unicode "icons" */ +static const teco_curses_icon_t teco_icons_file[] = { + {".Trash", 0xf1f8}, /* */ + {".atom", 0xe764}, /* */ + {".bash_history", 0xf489}, /* */ + {".bash_profile", 0xf489}, /* */ + {".bashrc", 0xf489}, /* */ + {".git", 0xf1d3}, /* */ + {".gitattributes", 0xf1d3}, /* */ + {".gitconfig", 0xf1d3}, /* */ + {".github", 0xf408}, /* */ + {".gitignore", 0xf1d3}, /* */ + {".gitmodules", 0xf1d3}, /* */ + {".rvm", 0xe21e}, /* */ + {".teco_ini", 0xedaa}, /* */ + {".teco_session", 0xedaa}, /* */ + {".vimrc", 0xe62b}, /* */ + {".vscode", 0xe70c}, /* */ + {".zshrc", 0xf489}, /* */ + {"COMMIT_EDITMSG", 0xf1d3}, /* */ + {"Cargo.lock", 0xe7a8}, /* */ + {"Dockerfile", 0xf308}, /* */ + {"GNUmakefile", 0xf489}, /* */ + {"MERGE_MSG", 0xf1d3}, /* */ + {"Makefile", 0xf489}, /* */ + {"PKGBUILD", 0xf303}, /* */ + {"TAG_EDITMSG", 0xf1d3}, /* */ + {"bin", 0xe5fc}, /* */ + {"config", 0xe5fc}, /* */ + {"docker-compose.yml", 0xf308}, /* */ + {"ds_store", 0xf179}, /* */ + {"git-rebase-todo", 0xf1d3}, /* */ + {"go.mod", 0xe626}, /* */ + {"go.sum", 0xe626}, /* */ + {"gradle", 0xe256}, /* */ + {"gruntfile.coffee", 0xe611}, /* */ + {"gruntfile.js", 0xe611}, /* */ + {"gruntfile.ls", 0xe611}, /* */ + {"gulpfile.coffee", 0xe610}, /* */ + {"gulpfile.js", 0xe610}, /* */ + {"gulpfile.ls", 0xe610}, /* */ + {"hidden", 0xf023}, /* */ + {"include", 0xe5fc}, /* */ + {"lib", 0xf121}, /* */ + {"localized", 0xf179}, /* */ + {"node_modules", 0xe718}, /* */ + {"npmignore", 0xe71e}, /* */ + {"rubydoc", 0xe73b}, /* */ + {"yarn.lock", 0xe718}, /* */ +}; + +/** Mapping of file extensions to Unicode "icons" */ +static const teco_curses_icon_t teco_icons_ext[] = { + {"DS_store", 0xf179}, /* */ + {"ai", 0xe7b4}, /* */ + {"android", 0xe70e}, /* */ + {"apk", 0xe70e}, /* */ + {"apple", 0xf179}, /* */ + {"avi", 0xf03d}, /* */ + {"avif", 0xf1c5}, /* */ + {"avro", 0xe60b}, /* */ + {"awk", 0xf489}, /* */ + {"bash", 0xf489}, /* */ + {"bat", 0xf17a}, /* */ + {"bats", 0xf489}, /* */ + {"bmp", 0xf1c5}, /* */ + {"bz", 0xf410}, /* */ + {"bz2", 0xf410}, /* */ + {"c", 0xe61e}, /* */ + {"c++", 0xe61d}, /* */ + {"cab", 0xe70f}, /* */ + {"cc", 0xe61d}, /* */ + {"cfg", 0xe615}, /* */ + {"class", 0xe256}, /* */ + {"clj", 0xe768}, /* */ + {"cljs", 0xe76a}, /* */ + {"cls", 0xf034}, /* */ + {"cmd", 0xe70f}, /* */ + {"coffee", 0xf0f4}, /* */ + {"conf", 0xe615}, /* */ + {"cp", 0xe61d}, /* */ + {"cpio", 0xf410}, /* */ + {"cpp", 0xe61d}, /* */ + {"cs", 0xf031b}, /* */ + {"csh", 0xf489}, /* */ + {"cshtml", 0xf1fa}, /* */ + {"csproj", 0xf031b}, /* */ + {"css", 0xe749}, /* */ + {"csv", 0xf1c3}, /* */ + {"csx", 0xf031b}, /* */ + {"cxx", 0xe61d}, /* */ + {"d", 0xe7af}, /* */ + {"dart", 0xe798}, /* */ + {"db", 0xf1c0}, /* */ + {"deb", 0xe77d}, /* */ + {"diff", 0xf440}, /* */ + {"djvu", 0xf02d}, /* */ + {"dll", 0xe70f}, /* */ + {"doc", 0xf1c2}, /* */ + {"docx", 0xf1c2}, /* */ + {"ds_store", 0xf179}, /* */ + {"dump", 0xf1c0}, /* */ + {"ebook", 0xe28b}, /* */ + {"ebuild", 0xf30d}, /* */ + {"editorconfig", 0xe615}, /* */ + {"ejs", 0xe618}, /* */ + {"elm", 0xe62c}, /* */ + {"env", 0xf462}, /* */ + {"eot", 0xf031}, /* */ + {"epub", 0xe28a}, /* */ + {"erb", 0xe73b}, /* */ + {"erl", 0xe7b1}, /* */ + {"ex", 0xe62d}, /* */ + {"exe", 0xf17a}, /* */ + {"exs", 0xe62d}, /* */ + {"fish", 0xf489}, /* */ + {"flac", 0xf001}, /* */ + {"flv", 0xf03d}, /* */ + {"font", 0xf031}, /* */ + {"fs", 0xe7a7}, /* */ + {"fsi", 0xe7a7}, /* */ + {"fsx", 0xe7a7}, /* */ + {"gdoc", 0xf1c2}, /* */ + {"gem", 0xe21e}, /* */ + {"gemfile", 0xe21e}, /* */ + {"gemspec", 0xe21e}, /* */ + {"gform", 0xf298}, /* */ + {"gif", 0xf1c5}, /* */ + {"go", 0xe626}, /* */ + {"gradle", 0xe256}, /* */ + {"groovy", 0xe775}, /* */ + {"gsheet", 0xf1c3}, /* */ + {"gslides", 0xf1c4}, /* */ + {"guardfile", 0xe21e}, /* */ + {"gz", 0xf410}, /* */ + {"h", 0xf0fd}, /* */ + {"hbs", 0xe60f}, /* */ + {"hpp", 0xf0fd}, /* */ + {"hs", 0xe777}, /* */ + {"htm", 0xf13b}, /* */ + {"html", 0xf13b}, /* */ + {"hxx", 0xf0fd}, /* */ + {"ico", 0xf1c5}, /* */ + {"image", 0xf1c5}, /* */ + {"img", 0xe271}, /* */ + {"iml", 0xe7b5}, /* */ + {"ini", 0xf17a}, /* */ + {"ipynb", 0xe678}, /* */ + {"iso", 0xe271}, /* */ + {"j2c", 0xf1c5}, /* */ + {"j2k", 0xf1c5}, /* */ + {"jad", 0xe256}, /* */ + {"jar", 0xe256}, /* */ + {"java", 0xe256}, /* */ + {"jfi", 0xf1c5}, /* */ + {"jfif", 0xf1c5}, /* */ + {"jif", 0xf1c5}, /* */ + {"jl", 0xe624}, /* */ + {"jmd", 0xf48a}, /* */ + {"jp2", 0xf1c5}, /* */ + {"jpe", 0xf1c5}, /* */ + {"jpeg", 0xf1c5}, /* */ + {"jpg", 0xf1c5}, /* */ + {"jpx", 0xf1c5}, /* */ + {"js", 0xe74e}, /* */ + {"json", 0xe60b}, /* */ + {"jsx", 0xe7ba}, /* */ + {"jxl", 0xf1c5}, /* */ + {"ksh", 0xf489}, /* */ + {"latex", 0xf034}, /* */ + {"less", 0xe758}, /* */ + {"lhs", 0xe777}, /* */ + {"license", 0xf0219}, /* */ + {"localized", 0xf179}, /* */ + {"lock", 0xf023}, /* */ + {"log", 0xf18d}, /* */ + {"lua", 0xe620}, /* */ + {"lz", 0xf410}, /* */ + {"lz4", 0xf410}, /* */ + {"lzh", 0xf410}, /* */ + {"lzma", 0xf410}, /* */ + {"lzo", 0xf410}, /* */ + {"m", 0xe61e}, /* */ + {"m4a", 0xf001}, /* */ + {"markdown", 0xf48a}, /* */ + {"md", 0xf48a}, /* */ + {"mjs", 0xe74e}, /* */ + {"mk", 0xf489}, /* */ + {"mkd", 0xf48a}, /* */ + {"mkv", 0xf03d}, /* */ + {"mm", 0xe61d}, /* */ + {"mobi", 0xe28b}, /* */ + {"mov", 0xf03d}, /* */ + {"mp3", 0xf001}, /* */ + {"mp4", 0xf03d}, /* */ + {"msi", 0xe70f}, /* */ + {"mustache", 0xe60f}, /* */ + {"nix", 0xf313}, /* */ + {"node", 0xf0399}, /* */ + {"npmignore", 0xe71e}, /* */ + {"odp", 0xf1c4}, /* */ + {"ods", 0xf1c3}, /* */ + {"odt", 0xf1c2}, /* */ + {"ogg", 0xf001}, /* */ + {"ogv", 0xf03d}, /* */ + {"otf", 0xf031}, /* */ + {"part", 0xf43a}, /* */ + {"patch", 0xf440}, /* */ + {"pdf", 0xf1c1}, /* */ + {"php", 0xe73d}, /* */ + {"pl", 0xe769}, /* */ + {"plx", 0xe769}, /* */ + {"pm", 0xe769}, /* */ + {"png", 0xf1c5}, /* */ + {"pod", 0xe769}, /* */ + {"ppt", 0xf1c4}, /* */ + {"pptx", 0xf1c4}, /* */ + {"procfile", 0xe21e}, /* */ + {"properties", 0xe60b}, /* */ + {"ps1", 0xf489}, /* */ + {"psd", 0xe7b8}, /* */ + {"pxm", 0xf1c5}, /* */ + {"py", 0xe606}, /* */ + {"pyc", 0xe606}, /* */ + {"r", 0xf25d}, /* */ + {"rakefile", 0xe21e}, /* */ + {"rar", 0xf410}, /* */ + {"razor", 0xf1fa}, /* */ + {"rb", 0xe21e}, /* */ + {"rdata", 0xf25d}, /* */ + {"rdb", 0xe76d}, /* */ + {"rdoc", 0xf48a}, /* */ + {"rds", 0xf25d}, /* */ + {"readme", 0xf48a}, /* */ + {"rlib", 0xe7a8}, /* */ + {"rmd", 0xf48a}, /* */ + {"rpm", 0xe7bb}, /* */ + {"rs", 0xe7a8}, /* */ + {"rspec", 0xe21e}, /* */ + {"rspec_parallel", 0xe21e}, /* */ + {"rspec_status", 0xe21e}, /* */ + {"rss", 0xf09e}, /* */ + {"rtf", 0xf0219}, /* */ + {"ru", 0xe21e}, /* */ + {"rubydoc", 0xe73b}, /* */ + {"sass", 0xe603}, /* */ + {"scala", 0xe737}, /* */ + {"scss", 0xe749}, /* */ + {"sh", 0xf489}, /* */ + {"shell", 0xf489}, /* */ + {"slim", 0xe73b}, /* */ + {"sln", 0xe70c}, /* */ + {"so", 0xf17c}, /* */ + {"sql", 0xf1c0}, /* */ + {"sqlite3", 0xe7c4}, /* */ + {"sty", 0xf034}, /* */ + {"styl", 0xe600}, /* */ + {"stylus", 0xe600}, /* */ + {"svg", 0xf1c5}, /* */ + {"swift", 0xe755}, /* */ + {"t", 0xe769}, /* */ + {"tar", 0xf410}, /* */ + {"taz", 0xf410}, /* */ + {"tbz", 0xf410}, /* */ + {"tbz2", 0xf410}, /* */ + {"tec", 0xedaa}, /* */ + {"tes", 0xedaa}, /* */ + {"tex", 0xf034}, /* */ + {"tgz", 0xf410}, /* */ + {"tiff", 0xf1c5}, /* */ + {"tlz", 0xf410}, /* */ + {"toml", 0xe615}, /* */ + {"torrent", 0xe275}, /* */ + {"ts", 0xe628}, /* */ + {"tsv", 0xf1c3}, /* */ + {"tsx", 0xe7ba}, /* */ + {"ttf", 0xf031}, /* */ + {"twig", 0xe61c}, /* */ + {"txt", 0xf15c}, /* */ + {"txz", 0xf410}, /* */ + {"tz", 0xf410}, /* */ + {"tzo", 0xf410}, /* */ + {"video", 0xf03d}, /* */ + {"vim", 0xe62b}, /* */ + {"vue", 0xf0844}, /* */ + {"war", 0xe256}, /* */ + {"wav", 0xf001}, /* */ + {"webm", 0xf03d}, /* */ + {"webp", 0xf1c5}, /* */ + {"windows", 0xf17a}, /* */ + {"woff", 0xf031}, /* */ + {"woff2", 0xf031}, /* */ + {"woman", 0xeaa4}, /* */ + {"xhtml", 0xf13b}, /* */ + {"xls", 0xf1c3}, /* */ + {"xlsx", 0xf1c3}, /* */ + {"xml", 0xf05c0}, /* */ + {"xul", 0xf05c0}, /* */ + {"xz", 0xf410}, /* */ + {"yaml", 0xf481}, /* */ + {"yml", 0xf481}, /* */ + {"zip", 0xf410}, /* */ + {"zsh", 0xf489}, /* */ + {"zsh-theme", 0xf489}, /* */ + {"zst", 0xf410}, /* */ +}; + +static int +teco_curses_icon_cmp(const void *a, const void *b) +{ + const gchar *str = a; + const teco_curses_icon_t *icon = b; + + return strcmp(str, icon->name); +} + +gunichar +teco_curses_icons_lookup_file(const gchar *filename) +{ + g_autofree gchar *basename = g_path_get_basename(filename); + const teco_curses_icon_t *icon; + + /* try to find icon by complete file name */ + icon = bsearch(basename, teco_icons_file, G_N_ELEMENTS(teco_icons_file), + sizeof(teco_icons_file[0]), teco_curses_icon_cmp); + if (icon) + return icon->c; + + /* try to find icon by extension */ + const gchar *ext = strrchr(basename, '.'); + if (ext) { + icon = bsearch(ext+1, teco_icons_ext, G_N_ELEMENTS(teco_icons_ext), + sizeof(teco_icons_ext[0]), teco_curses_icon_cmp); + return icon ? icon->c : 0xf15b; /* */ + } + + /* default file icon for files without extension */ + return 0xf016; /* */ +} + +gunichar +teco_curses_icons_lookup_dir(const gchar *dirname) +{ + g_autofree gchar *basename = g_path_get_basename(dirname); + const teco_curses_icon_t *icon; + + icon = bsearch(basename, teco_icons_file, G_N_ELEMENTS(teco_icons_file), + sizeof(teco_icons_file[0]), teco_curses_icon_cmp); + + /* default folder icon */ + return icon ? icon->c : 0xf115; /* */ +} diff --git a/src/interface-curses/curses-icons.h b/src/interface-curses/curses-icons.h new file mode 100644 index 0000000..c1be06f --- /dev/null +++ b/src/interface-curses/curses-icons.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2012-2024 Robin Haberkorn + * + * This program 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. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + */ +#pragma once + +#include <glib.h> + +/** + * Q-Register icon. + * 0xf04cf would look more similar to the current Gtk icon. + */ +#define TECO_CURSES_ICONS_QREG 0xe236 /* */ + +gunichar teco_curses_icons_lookup_file(const gchar *filename); +gunichar teco_curses_icons_lookup_dir(const gchar *dirname); diff --git a/src/interface-curses/curses-info-popup.c b/src/interface-curses/curses-info-popup.c index a738f5d..e6e1549 100644 --- a/src/interface-curses/curses-info-popup.c +++ b/src/interface-curses/curses-info-popup.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2023 Robin Haberkorn + * Copyright (C) 2012-2024 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,6 +28,7 @@ #include "interface.h" #include "curses-utils.h" #include "curses-info-popup.h" +#include "curses-icons.h" /* * FIXME: This is redundant with gtk-info-popup.c. @@ -75,8 +76,13 @@ teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr) gint pad_cols; /**! entry columns */ gint pad_colwidth; /**! width per entry column */ - /* reserve 2 spaces between columns */ - pad_colwidth = MIN(ctx->longest + 2, cols - 2); + /* + * With Unicode icons enabled, we reserve 2 characters at the beginning and one + * after the filename/directory. + * Otherwise 2 characters after the entry. + */ + gint reserve = teco_ed & TECO_ED_ICONS ? 2+1 : 2; + pad_colwidth = MIN(ctx->longest + reserve, cols - 2); /* pad_cols = floor((cols - 2) / pad_colwidth) */ pad_cols = (cols - 2) / pad_colwidth; @@ -111,8 +117,19 @@ teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr) switch (entry->type) { case TECO_POPUP_FILE: + g_assert(!teco_string_contains(&entry->name, '\0')); + if (teco_ed & TECO_ED_ICONS) { + teco_curses_add_wc(ctx->pad, teco_curses_icons_lookup_file(entry->name.data)); + waddch(ctx->pad, ' '); + } + teco_curses_format_filename(ctx->pad, entry->name.data, -1); + break; case TECO_POPUP_DIRECTORY: g_assert(!teco_string_contains(&entry->name, '\0')); + if (teco_ed & TECO_ED_ICONS) { + teco_curses_add_wc(ctx->pad, teco_curses_icons_lookup_dir(entry->name.data)); + waddch(ctx->pad, ' '); + } teco_curses_format_filename(ctx->pad, entry->name.data, -1); break; default: diff --git a/src/interface-curses/curses-info-popup.h b/src/interface-curses/curses-info-popup.h index bcdb3b8..a6c28a5 100644 --- a/src/interface-curses/curses-info-popup.h +++ b/src/interface-curses/curses-info-popup.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2023 Robin Haberkorn + * Copyright (C) 2012-2024 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-curses/curses-utils.c b/src/interface-curses/curses-utils.c index 8dc62f1..c751afd 100644 --- a/src/interface-curses/curses-utils.c +++ b/src/interface-curses/curses-utils.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2023 Robin Haberkorn + * Copyright (C) 2012-2024 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,7 +29,21 @@ #include "string-utils.h" #include "curses-utils.h" -gsize +/** + * Render UTF-8 string with TECO character representations. + * + * Strings are cut off with `...` at the end if necessary. + * The mapping is similar to teco_view_set_representations(). + * + * @param win The Curses window to write to. + * @param str The string to format. + * @param len The length of the string in bytes. + * @param max_width The maximum width to consume in + * the window in characters. If smaller 0, take the + * entire remaining space in the window. + * @return Number of characters actually written. + */ +guint teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width) { int old_x, old_y; @@ -42,6 +56,12 @@ teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width) while (len > 0) { /* + * NOTE: It shouldn't be possible to meet any string, + * that is not valid UTF-8. + */ + gsize clen = g_utf8_next_char(str) - str; + + /* * NOTE: This mapping is similar to * teco_view_set_representations(). */ @@ -85,12 +105,18 @@ teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width) chars_added++; if (chars_added > max_width) goto truncate; - waddch(win, *str); + /* + * FIXME: This works with UTF-8 on ncurses, + * since it detects multi-byte characters. + * However on other platforms wadd_wch() may be + * necessary, which requires a widechar Curses variant. + */ + waddnstr(win, str, clen); } } - str++; - len--; + str += clen; + len -= clen; } return getcurx(win) - old_x; @@ -108,23 +134,43 @@ truncate: return getcurx(win) - old_x; } -gsize -teco_curses_format_filename(WINDOW *win, const gchar *filename, - gint max_width) +/** + * Render UTF-8 filename. + * + * This cuts of overlong filenames with `...` at the beginning, + * possibly skipping any drive letter. + * Control characters are escaped, but not highlighted. + * + * @param win The Curses window to write to. + * @param filename Null-terminated filename to render. + * @param max_width The maximum width to consume in + * the window in characters. If smaller 0, take the + * entire remaining space in the window. + * @return Number of characters actually written. + */ +guint +teco_curses_format_filename(WINDOW *win, const gchar *filename, gint max_width) { int old_x = getcurx(win); g_autofree gchar *filename_printable = teco_string_echo(filename, strlen(filename)); - size_t filename_len = strlen(filename_printable); + glong filename_len = g_utf8_strlen(filename_printable, -1); if (max_width < 0) max_width = getmaxx(win) - old_x; - if (filename_len <= (size_t)max_width) { + if (filename_len <= max_width) { + /* + * FIXME: This works with UTF-8 on ncurses, + * since it detects multi-byte characters. + * However on other platforms wadd_wch() may be + * necessary, which requires a widechar Curses variant. + */ waddstr(win, filename_printable); - } else { - const gchar *keep_post = filename_printable + filename_len - - max_width + 3; + } else if (filename_len >= 3) { + const gchar *keep_post; + keep_post = g_utf8_offset_to_pointer(filename_printable + strlen(filename_printable), + -max_width + 3); #ifdef G_OS_WIN32 const gchar *keep_pre = g_path_skip_root(filename_printable); diff --git a/src/interface-curses/curses-utils.h b/src/interface-curses/curses-utils.h index a91ab44..2c819ee 100644 --- a/src/interface-curses/curses-utils.h +++ b/src/interface-curses/curses-utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2023 Robin Haberkorn + * Copyright (C) 2012-2024 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,6 +20,17 @@ #include <curses.h> -gsize teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width); +guint teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width); -gsize teco_curses_format_filename(WINDOW *win, const gchar *filename, gint max_width); +guint teco_curses_format_filename(WINDOW *win, const gchar *filename, gint max_width); + +/** + * Add Unicode character to window. + * This is just like wadd_wch(), but does not require wide-char APIs. + */ +static inline void +teco_curses_add_wc(WINDOW *win, gunichar chr) +{ + gchar buf[6]; + waddnstr(win, buf, g_unichar_to_utf8(chr, buf)); +} diff --git a/src/interface-curses/interface.c b/src/interface-curses/interface.c index ef3f0c7..95e86c9 100644 --- a/src/interface-curses/interface.c +++ b/src/interface-curses/interface.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2023 Robin Haberkorn + * Copyright (C) 2012-2024 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,7 +24,6 @@ #include <stdlib.h> #include <stdarg.h> #include <unistd.h> -#include <locale.h> #include <errno.h> #ifdef HAVE_WINDOWS_H @@ -65,11 +64,12 @@ #include "qreg.h" #include "ring.h" #include "error.h" -#include "curses-utils.h" -#include "curses-info-popup.h" #include "view.h" #include "memory.h" #include "interface.h" +#include "curses-utils.h" +#include "curses-info-popup.h" +#include "curses-icons.h" #if defined(__PDCURSES__) && defined(G_OS_WIN32) && \ !defined(PDCURSES_GUI) @@ -340,12 +340,18 @@ static struct { TECO_INFO_TYPE_QREG } info_type; teco_string_t info_current; + gboolean info_dirty; WINDOW *msg_window; WINDOW *cmdline_window, *cmdline_pad; - gsize cmdline_len, cmdline_rubout_len; + guint cmdline_len, cmdline_rubout_len; + /** + * Pad used exclusively for wgetch() as it will not + * result in unwanted wrefresh(). + */ + WINDOW *input_pad; GQueue *input_queue; teco_curses_info_popup_t popup; @@ -554,7 +560,7 @@ teco_interface_init_screen(void) g_assert(teco_interface.screen_tty != NULL); teco_interface.screen = newterm(NULL, teco_interface.screen_tty, teco_interface.screen_tty); - if (!teco_interface.screen) { + if (G_UNLIKELY(!teco_interface.screen)) { g_fprintf(stderr, "Error initializing interactive mode. " "$TERM may be incorrect.\n"); exit(EXIT_FAILURE); @@ -629,28 +635,6 @@ teco_interface_init_interactive(GError **error) return FALSE; /* - * On UNIX terminals, the escape key is usually - * delivered as the escape character even though function - * keys are delivered as escape sequences as well. - * That's why there has to be a timeout for detecting - * escape presses if function key handling is enabled. - * This timeout can be controlled using $ESCDELAY on - * ncurses but its default is much too long. - * We set it to 25ms as Vim does. In the very rare cases - * this won't suffice, $ESCDELAY can still be set explicitly. - * - * NOTE: The only terminal emulator I'm aware of that lets - * us send an escape sequence for the escape key is Mintty - * (see "\e[?7727h"). - * - * FIXME: This appears to be ineffective for netbsd-curses. - */ -#ifdef CURSES_TTY - if (!g_getenv("ESCDELAY")) - set_escdelay(25); -#endif - - /* * $TERM must be unset or "#win32con" for the win32 * driver to load. * So we always ignore any $TERM changes by the user. @@ -679,12 +663,31 @@ teco_interface_init_interactive(GError **error) PDC_set_function_key(FUNCTION_KEY_SHUT_DOWN, KEY_CLOSE); #endif - /* for displaying UTF-8 characters properly */ - setlocale(LC_CTYPE, ""); - teco_interface_init_screen(); /* + * On UNIX terminals, the escape key is usually + * delivered as the escape character even though function + * keys are delivered as escape sequences as well. + * That's why there has to be a timeout for detecting + * escape presses if function key handling is enabled. + * This timeout can be controlled using $ESCDELAY on + * ncurses but its default is much too long. + * We set it to 25ms as Vim does. In the very rare cases + * this won't suffice, $ESCDELAY can still be set explicitly. + * + * NOTE: The only terminal emulator I'm aware of that lets + * us send an escape sequence for the escape key is Mintty + * (see "\e[?7727h"). + * + * NOTE: The delay is overwritten by initscr() on netbsd-curses. + */ +#ifdef CURSES_TTY + if (!g_getenv("ESCDELAY")) + set_escdelay(25); +#endif + + /* * We always have a CTRL handler on Windows, but doing it * here again, ensures that we have a higher precedence * than the one installed by PDCurses. @@ -699,12 +702,22 @@ teco_interface_init_interactive(GError **error) curs_set(0); teco_interface.info_window = newwin(1, 0, 0, 0); - teco_interface.msg_window = newwin(1, 0, LINES - 2, 0); - teco_interface.cmdline_window = newwin(0, 0, LINES - 1, 0); - keypad(teco_interface.cmdline_window, TRUE); - nodelay(teco_interface.cmdline_window, TRUE); + + teco_interface.input_pad = newpad(1, 1); + /* + * Controlling function key processing is important + * on Unix Curses, as ESCAPE is handled as the beginning + * of a escape sequence when terminal emulators are + * involved. + * Still, it's now enabled always since the ESCDELAY + * workaround works nicely. + * On some Curses variants (XCurses) keypad + * must always be TRUE so we receive KEY_RESIZE. + */ + keypad(teco_interface.input_pad, TRUE); + nodelay(teco_interface.input_pad, TRUE); teco_interface.input_queue = g_queue_new(); @@ -748,8 +761,8 @@ teco_interface_restore_batch(void) * Set window title to a reasonable default, * in case it is not reset immediately by the * shell. - * FIXME: See set_window_title() why this - * is necessary. + * FIXME: See teco_interface_set_window_title() + * why this is necessary. */ #if defined(CURSES_TTY) && defined(HAVE_TIGETSTR) teco_interface_set_window_title(g_getenv("TERM") ? : ""); @@ -978,10 +991,14 @@ teco_interface_draw_info(void) const gchar *info_type_str; + waddstr(teco_interface.info_window, PACKAGE_NAME " "); + switch (teco_interface.info_type) { case TECO_INFO_TYPE_QREG: info_type_str = PACKAGE_NAME " - <QRegister> "; - waddstr(teco_interface.info_window, info_type_str); + teco_curses_add_wc(teco_interface.info_window, + teco_ed & TECO_ED_ICONS ? TECO_CURSES_ICONS_QREG : '-'); + waddstr(teco_interface.info_window, " <QRegister> "); /* same formatting as in command lines */ teco_curses_format_str(teco_interface.info_window, teco_interface.info_current.data, @@ -990,10 +1007,15 @@ teco_interface_draw_info(void) case TECO_INFO_TYPE_BUFFER: info_type_str = PACKAGE_NAME " - <Buffer> "; - waddstr(teco_interface.info_window, info_type_str); g_assert(!teco_string_contains(&teco_interface.info_current, '\0')); + teco_curses_add_wc(teco_interface.info_window, + teco_ed & TECO_ED_ICONS ? teco_curses_icons_lookup_file(teco_interface.info_current.data) : '-'); + waddstr(teco_interface.info_window, " <Buffer> "); teco_curses_format_filename(teco_interface.info_window, - teco_interface.info_current.data, -1); + teco_interface.info_current.data, + getmaxx(teco_interface.info_window) - + getcurx(teco_interface.info_window) - 1); + waddch(teco_interface.info_window, teco_interface.info_dirty ? '*' : ' '); break; default: @@ -1003,13 +1025,13 @@ teco_interface_draw_info(void) wclrtoeol(teco_interface.info_window); /* - * Make sure the title will consist only of printable - * characters + * Make sure the title will consist only of printable characters. */ g_autofree gchar *info_current_printable; info_current_printable = teco_string_echo(teco_interface.info_current.data, teco_interface.info_current.len); - g_autofree gchar *title = g_strconcat(info_type_str, info_current_printable, NULL); + g_autofree gchar *title = g_strconcat(info_type_str, info_current_printable, + teco_interface.info_dirty ? "*" : "", NULL); teco_interface_set_window_title(title); } @@ -1019,6 +1041,7 @@ teco_interface_info_update_qreg(const teco_qreg_t *reg) teco_string_clear(&teco_interface.info_current); teco_string_init(&teco_interface.info_current, reg->head.name.data, reg->head.name.len); + teco_interface.info_dirty = FALSE; teco_interface.info_type = TECO_INFO_TYPE_QREG; /* NOTE: drawn in teco_interface_event_loop_iter() */ } @@ -1030,8 +1053,7 @@ teco_interface_info_update_buffer(const teco_buffer_t *buffer) teco_string_clear(&teco_interface.info_current); teco_string_init(&teco_interface.info_current, filename, strlen(filename)); - teco_string_append_c(&teco_interface.info_current, - buffer->dirty ? '*' : ' '); + teco_interface.info_dirty = buffer->dirty; teco_interface.info_type = TECO_INFO_TYPE_BUFFER; /* NOTE: drawn in teco_interface_event_loop_iter() */ } @@ -1044,7 +1066,8 @@ teco_interface_cmdline_update(const teco_cmdline_t *cmdline) * We don't know if it is similar to the last one, * so resizing makes no sense. * We approximate the size of the new formatted command-line, - * wasting a few bytes for control characters. + * wasting a few bytes for control characters and + * multi-byte Unicode sequences. */ if (teco_interface.cmdline_pad) delwin(teco_interface.cmdline_pad); @@ -1172,7 +1195,7 @@ teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, { int rc = str ? PDC_setclipboard(str, str_len) : PDC_clearclipboard(); if (rc != PDC_CLIP_SUCCESS) { - g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + g_set_error(error, TECO_ERROR, TECO_ERROR_CLIPBOARD, "Error %d copying to clipboard", rc); return FALSE; } @@ -1194,7 +1217,7 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError if (rc == PDC_CLIP_EMPTY) return TRUE; if (rc != PDC_CLIP_SUCCESS) { - g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + g_set_error(error, TECO_ERROR, TECO_ERROR_CLIPBOARD, "Error %d retrieving clipboard", rc); return FALSE; } @@ -1232,9 +1255,17 @@ teco_interface_init_clipboard(void) * must be enabled. * There is no way to find out if they are but we must * not register the clipboard registers if they aren't. - * Therefore, a special XTerm clipboard ED flag an be set by the user. + * Still, XTerm clipboards are broken with Unicode characters. + * Also, there are other terminal emulators supporting OSC-52, + * so the XTerm version is only checked if the terminal identifies as XTerm. + * Also, a special clipboard ED flag must be set by the user. + * + * NOTE: Apparently there is also a terminfo entry Ms, but it's probably + * not worth using it since it won't always be set and even if set, does not + * tell you whether the terminal will actually answer to the escape sequence or not. */ - if (!(teco_ed & TECO_ED_XTERM_CLIPBOARD) || teco_xterm_version() < 203) + if (!(teco_ed & TECO_ED_OSC52) || + (teco_xterm_version() >= 0 && teco_xterm_version() < 203)) return; teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); @@ -1300,6 +1331,8 @@ teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, gboolean teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error) { + gboolean ret = TRUE; + /* * Query the clipboard -- XTerm will reply with the * OSC-52 command that would set the current selection. @@ -1320,18 +1353,19 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError * to be on the safe side. */ halfdelay(1); /* 100ms timeout */ - keypad(stdscr, FALSE); + /* don't interpret escape sequences */ + keypad(teco_interface.input_pad, FALSE); /* * Skip "\e]52;x;" (7 characters). */ for (gint i = 0; i < 7; i++) { - if (getch() == ERR) { + ret = wgetch(teco_interface.input_pad) != ERR; + if (!ret) { /* timeout */ - cbreak(); - g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CLIPBOARD, "Timed out reading XTerm clipboard"); - return FALSE; + goto cleanup; } } @@ -1347,17 +1381,22 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError */ gchar buffer[MAX(3, 7)]; - gchar c = (gchar)getch(); - if (c == ERR) { + gchar c = (gchar)wgetch(teco_interface.input_pad); + ret = c != ERR; + if (!ret) { /* timeout */ - cbreak(); g_string_free(str_base64, TRUE); - g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CLIPBOARD, "Timed out reading XTerm clipboard"); - return FALSE; + goto cleanup; } if (c == '\a') break; + if (c == '\e') { + /* OSC escape sequence can also be terminated by "\e\\" */ + c = (gchar)wgetch(teco_interface.input_pad); + break; + } /* * This could be simplified using sscanf() and @@ -1372,14 +1411,16 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError g_string_append_len(str_base64, buffer, out_len); } - cbreak(); - if (str) *str = str_base64->str; *len = str_base64->len; g_string_free(str_base64, !str); - return TRUE; + +cleanup: + keypad(teco_interface.input_pad, TRUE); + nodelay(teco_interface.input_pad, TRUE); + return ret; } #else /* !PDCURSES && !CURSES_TTY */ @@ -1489,13 +1530,17 @@ teco_interface_is_interrupted(void) gboolean teco_interface_is_interrupted(void) { - if (!teco_interface.cmdline_window) + if (!teco_interface.input_pad) /* batch mode */ return teco_interrupted != FALSE; - /* NOTE: getch() is configured to be nonblocking. */ + /* + * NOTE: wgetch() is configured to be nonblocking. + * We wgetch() on a dummy pad, so this does not call any + * wrefresh(). + */ gint key; - while ((key = wgetch(teco_interface.cmdline_window)) != ERR) { + while ((key = wgetch(teco_interface.input_pad)) != ERR) { if (G_UNLIKELY(key == TECO_CTL_KEY('C'))) return TRUE; g_queue_push_tail(teco_interface.input_queue, @@ -1535,35 +1580,19 @@ teco_interface_refresh(void) static gint teco_interface_blocking_getch(void) { - /* - * Setting function key processing is important - * on Unix Curses, as ESCAPE is handled as the beginning - * of a escape sequence when terminal emulators are - * involved. - * On some Curses variants (XCurses) however, keypad - * must always be TRUE so we receive KEY_RESIZE. - * - * FIXME: NetBSD's curses could be handled like ncurses, - * but gets into an undefined state when SciTECO processes - * escape sequences. - */ -#ifdef NCURSES_UNIX - keypad(teco_interface.cmdline_window, teco_ed & TECO_ED_FNKEYS); -#endif - /* no special <CTRL/C> handling */ raw(); - nodelay(teco_interface.cmdline_window, FALSE); + nodelay(teco_interface.input_pad, FALSE); /* * Memory limiting is stopped temporarily, since it might otherwise * constantly place 100% load on the CPU. */ teco_memory_stop_limiting(); - gint key = wgetch(teco_interface.cmdline_window); + gint key = wgetch(teco_interface.input_pad); teco_memory_start_limiting(); /* allow asynchronous interruptions on <CTRL/C> */ teco_interrupted = FALSE; - nodelay(teco_interface.cmdline_window, TRUE); + nodelay(teco_interface.input_pad, TRUE); #if defined(CURSES_TTY) || defined(PDCURSES_WINCON) || defined(NCURSES_WIN32) noraw(); /* FIXME: necessary because of NCURSES_WIN32 bug */ cbreak(); @@ -1585,6 +1614,11 @@ teco_interface_blocking_getch(void) void teco_interface_event_loop_iter(void) { + static gchar keybuf[4]; + static gint keybuf_i = 0; + + GError **error = &teco_interface.event_loop_error; + gint key = g_queue_is_empty(teco_interface.input_queue) ? teco_interface_blocking_getch() : GPOINTER_TO_INT(g_queue_pop_head(teco_interface.input_queue)); @@ -1613,23 +1647,24 @@ teco_interface_event_loop_iter(void) * backspace. * In SciTECO backspace is normalized to ^H. */ - if (!teco_cmdline_keypress_c(TECO_CTL_KEY('H'), - &teco_interface.event_loop_error)) + if (!teco_cmdline_keymacro_c(TECO_CTL_KEY('H'), error)) return; break; case KEY_ENTER: case '\r': case '\n': - if (!teco_cmdline_keypress_c('\n', &teco_interface.event_loop_error)) + if (!teco_cmdline_keymacro_c('\n', error)) return; break; /* * Function key macros + * + * FIXME: Perhaps support everything returned by keyname()? */ #define FN(KEY) \ case KEY_##KEY: \ - if (!teco_cmdline_fnmacro(#KEY, &teco_interface.event_loop_error)) \ + if (!teco_cmdline_keymacro(#KEY, -1, error)) \ return; \ break #define FNS(KEY) FN(KEY); FN(S##KEY) @@ -1639,9 +1674,8 @@ teco_interface_event_loop_iter(void) gchar macro_name[3+1]; g_snprintf(macro_name, sizeof(macro_name), - "F%d", key - KEY_F0); - if (!teco_cmdline_fnmacro(macro_name, - &teco_interface.event_loop_error)) + "F%d", key - KEY_F0); + if (!teco_cmdline_keymacro(macro_name, -1, error)) return; break; } @@ -1660,9 +1694,31 @@ teco_interface_event_loop_iter(void) * Control keys and keys with printable representation */ default: - if (key < 0x80 && - !teco_cmdline_keypress_c(key, &teco_interface.event_loop_error)) + if (key > 0xFF) + /* unhandled function key */ return; + + /* + * NOTE: There's also wget_wch(), but it requires + * a widechar version of Curses. + */ + keybuf[keybuf_i++] = key; + gsize len = keybuf_i; + gunichar cp = g_utf8_get_char_validated(keybuf, len); + if (keybuf_i >= sizeof(keybuf) || cp != (gunichar)-2) + keybuf_i = 0; + if ((gint32)cp < 0) + /* incomplete or invalid */ + return; + switch (teco_cmdline_keymacro(keybuf, len, error)) { + case TECO_KEYMACRO_ERROR: + return; + case TECO_KEYMACRO_SUCCESS: + break; + case TECO_KEYMACRO_UNDEFINED: + if (!teco_cmdline_keypress(keybuf, len, error)) + return; + } } teco_interface_refresh(); @@ -1733,6 +1789,8 @@ teco_interface_cleanup(void) delwin(teco_interface.cmdline_pad); if (teco_interface.msg_window) delwin(teco_interface.msg_window); + if (teco_interface.input_pad) + delwin(teco_interface.input_pad); /* * PDCurses/WinCon crashes if initscr() wasn't called. |