From 3acf497ecf360459f314f8db3bda6ae564dcca0c Mon Sep 17 00:00:00 2001 From: Mattias Andrée Date: Sun, 21 Dec 2025 20:15:54 +0100 Subject: First commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Andrée --- .gitignore | 15 +++ LICENSE | 15 +++ Makefile | 37 ++++++ README | 46 ++++++++ cmap.1 | 114 ++++++++++++++++++ cmap.c | 390 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ common.h | 28 +++++ config.mk | 8 ++ 8 files changed, 653 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 cmap.1 create mode 100644 cmap.c create mode 100644 common.h create mode 100644 config.mk diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60727d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*\#* +*~ +*.o +*.a +*.lo +*.su +*.so +*.so.* +*.dll +*.dylib +*.gch +*.gcov +*.gcno +*.gcda +/cmap diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e6be1c --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +© 2025 Mattias Andrée + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c3784c4 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +.POSIX: + +CONFIGFILE = config.mk +include $(CONFIGFILE) + +OBJ =\ + cmap.o + +HDR = common.h + +all: cmap +$(OBJ): $(HDR) + +.c.o: + $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS) + +cmap: $(OBJ) + $(CC) -o $@ $(OBJ) $(LDFLAGS) + +install: cmap + mkdir -p -- "$(DESTDIR)$(PREFIX)/bin" + mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/" + cp -- cmap "$(DESTDIR)$(PREFIX)/bin/" + cp -- cmap.1 "$(DESTDIR)$(MANPREFIX)/man1/" + +uninstall: + -rm -f -- "$(DESTDIR)$(PREFIX)/bin/cmap" + -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/cmap.1" + +clean: + -rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda + -rm -f -- cmap + +.SUFFIXES: +.SUFFIXES: .o .c + +.PHONY: all install uninstall clean diff --git a/README b/README new file mode 100644 index 0000000..ea051ab --- /dev/null +++ b/README @@ -0,0 +1,46 @@ +NAME + cmap - character selection utility for the terminal + +SYNOPSIS + cmap [-f font-family] [-s [font-size][/[[min]-[max]]]] [-B | -S] [-bi] + +DESCRIPTION + cmap is an interactive terminal utility for browsing and copying + characters. + +OPTIONS + The cmap utility conforms to the Base Definitions + volume of POSIX.1-2017, Section 12.2, Utility Syntax Guidelines. + + The following options are supported: + + -B + Use listing by Unicode block by default. + + -b + Use bold font by default + + -f font-family + Use font-family as the default font family. + + -i + Use italic or oblique font by default. + + -S + Use listing by script by default. + + -s [font-size][/[[min]-[max]]] + Set the default font size, in the character + table, to font-size. + + Set the minimum font size, in the character + table, to min. + + Set the maximum font size, in the character + table, to max. + +OPERANDS + No operands are supported. + +SEE ALSO + gcmap(3) diff --git a/cmap.1 b/cmap.1 new file mode 100644 index 0000000..d803d40 --- /dev/null +++ b/cmap.1 @@ -0,0 +1,114 @@ +.TH CMAP 1 cmap +.SH NAME +cmap \- character selection utility for the terminal + +.SH SYNOPSIS +.B cmap +[-f +.IR font-family ] +[-s +.RI [ font-size ][\fB/\fP[[ min ]\fB\-\fP[ max ]]]] +[-B | -S] [-bi] + +.SH DESCRIPTION +.B cmap +is an interactive terminal utility for browsing and +copying characters. + +.SH OPTIONS +The +.B cmap +utility conforms to the Base Definitions +volume of POSIX.1-2017, +.IR "Section 12.2" , +.IR "Utility Syntax Guidelines" . +.PP +The following options are supported: +.TP +.B -B +Use listing by Unicode block by default. +.TP +.B -b +Use bold font by default +.TP +.BR -f \ \fIfont-family\fP +Use +.I font-family +as the default font family. +.TP +.B -i +Use italic or oblique font by default. +.TP +.B -S +Use listing by script by default. +.TP +.RI \fB-s\fP\ [ font-size ][\fB/\fP[[ min ]\fB-\fP[ max ]]] +Set the default font size, in the character +table, to +.IR font-size . + +Set the minimum font size, in the character +table, to +.IR min . + +Set the maximum font size, in the character +table, to +.IR max . + +.SH OPERANDS +No operands are supported. + +.SH ENVIRONMENT VARIABLES +The execution of +.B cmap +is affected by environment variables that affects the +.BR libterminput (7) +library. + +.SH INTERACTION +The following keyboard actions are globally recognised: +.TP +.B Ctrl+Q +.TQ +.B Ctrl+C +Quit. +.TP +.B Ctrl+L +Redraw program. +.TP +.B Ctrl+Z +Put process in background. +.TP +.B Meta+Left +Make side-pane smaller. +.TP +.B Meta+Right +Make side-pane larger. +.TP +.B Tab +Cycle, forwards, between interface elements. +.TP +.B Backtab +Cycle, backward, between interface elements. +.PP +The following keyboard actions are recognised when +a list or table is focused: +.TP +.B Shift+Up +.TQ +.B Shift+PageUp +Scroll up without changing selected element. +.TP +.B Shift+Down +.TQ +.B Shift+PageDown +Scroll down without changing selected element. +.TP +.B Shift+Left +Scroll left without changing selected element. +.TP +.B Shift+Right +Scroll right without changing selected element. + +.SH SEE ALSO +.BR gcmap (1) diff --git a/cmap.c b/cmap.c new file mode 100644 index 0000000..66ac2fd --- /dev/null +++ b/cmap.c @@ -0,0 +1,390 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +USAGE("[-f font-family] [-s [font-size][/[[min]-[max]]]] [-B | -S] [-bi]"); + + +#define DEFAULT_LISTING_TYPE "script" +#define LIST_LISTINGS(X, D)\ + X(BY_SCRIPT, "By _script", DEFAULT_LISTING_TYPE, by_script_selected, populate_scripts, GDK_s) D\ + X(BY_BLOCK, "By Unicode _block", "block", by_block_selected, populate_blocks, GDK_b) + +enum listing { +#define X(ENUM, TITLE, TYPE, SELFUN, POPFUN, ACCEL) ENUM + LIST_LISTINGS(X, COMMA) +#undef X +}; + +#define NLISTINGS_X(...) (size_t)1 +#define NLISTINGS (LIST_LISTINGS(NLISTINGS_X, +)) + + +static const struct libcmap_block all_block = {.name = "All", .range = LIBCMAP_UNIVERSE_RANGE}; + +static unsigned int min_font_size = 4; +static unsigned int default_font_size = 22; +static unsigned int max_font_size = 500; +static unsigned int small_font_size_increment = 1; +static unsigned int big_font_size_increment = 8; + +static enum listing listing = (enum listing)0; +static int use_bold = 0; +static int use_italic = 0; + +static size_t term_width; +static size_t term_height; +static size_t left_pane_width; +static int automatic_left_pane_width = 1; + +static volatile sig_atomic_t term_resized = 0; +static struct termios term_attributes_stdin; +static int term_attributes_stdin_saved = 0; +static int term_entered_submode_stdout = 0; + +static int interrupt_deferred = 0; +static int exiting = 0; + + +static void +enter_subterminal(void) +{ + printf("\033[?1049h" /* enter subterminal */ + "\033[?25l" /* hide cursor */ + "\033[H\033[2J" /* clear terminal */ + ); + fflush(stdout); + term_entered_submode_stdout = 1; +} + + +static void +exit_subterminal(void) +{ + if (term_entered_submode_stdout) { + term_entered_submode_stdout = 0; + printf("\033[H\033[2J" /* clear terminal */ + "\033[?25h" /* show cursor */ + "\033[?1049l" /* exit subterminal */ + ); + fflush(stdout); + } +} + + +static void +set_term_attributes(void) +{ + struct termios new_term_attributes_stdin; + + if (tcgetattr(STDIN_FILENO, &term_attributes_stdin)) + eprintf("tcgetattr :"); + term_attributes_stdin_saved = 1; + + memcpy(&new_term_attributes_stdin, &term_attributes_stdin, sizeof(term_attributes_stdin)); + new_term_attributes_stdin.c_iflag &= (tcflag_t)~(IXON | IXANY | IXOFF); + new_term_attributes_stdin.c_lflag &= (tcflag_t)~(ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHONL); + + if (tcsetattr(STDIN_FILENO, TCSANOW, &new_term_attributes_stdin)) + weprintf("tcsetattr TCSANOW:"); +} + + +static void +restore_term_attributes(void) +{ + if (term_attributes_stdin_saved) { + term_attributes_stdin_saved = 0; + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attributes_stdin)) + weprintf("tcsetattr TCSAFLUSH:"); + } +} + + +static void +configure_terminal(void) +{ + set_term_attributes(); + enter_subterminal(); +} + + +static void +restore_terminal(void) +{ + restore_term_attributes(); + exit_subterminal(); +} + + +static void +cleanup(void) +{ + restore_terminal(); +} + + +static void +term_resized_callback(int signo) +{ + (void) signo; + term_resized = 1; +} + + +static void +subscribe_term_resize(void) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = &term_resized_callback; + + if (sigaction(SIGWINCH, &sa, NULL)) + eprintf("sigaction SIGWINCH:"); +} + + +static void +update_term_size(void) +{ + struct winsize ws; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws)) + eprintf("ioclt TIOCGWINSZ:"); + + term_width = ws.ws_col; + term_height = ws.ws_row; +} + + +static const char * +maybe_sat_parse_int_as_uint(const char *s, unsigned int *out, int *setp) +{ + unsigned int digit; + if (!isdigit(*s)) + return s; + *out = 0; + *setp = 1; + for (; isdigit(*s); s++) { + digit = (unsigned int)(*s & 15); + if (*out > ((unsigned int)INT_MAX - digit) / 10U) + *out = (unsigned int)INT_MAX; + else + *out = *out * 10U + digit; + } + return s; +} + + +static void +parse_fontsize(const char *s, int *default_font_size_setp, int *min_font_size_setp, int *max_font_size_setp) +{ + s = maybe_sat_parse_int_as_uint(s, &default_font_size, default_font_size_setp); + + if (!*s) + return; + if (*s++ != '/') + usage(); + if (!*s) + return; + + s = maybe_sat_parse_int_as_uint(s, &min_font_size, min_font_size_setp); + + if (*s++ != '-') + usage(); + if (!*s) + return; + + s = maybe_sat_parse_int_as_uint(s, &max_font_size, max_font_size_setp); + + if (*s) + usage(); + + if (!default_font_size) + eprintf("default font size cannot be zero"); + if (!min_font_size) + eprintf("minimum font size cannot be zero"); + if (!max_font_size) + eprintf("maximum font size cannot be zero"); +} + + +static void +handle_keyboard_input(union libterminput_input *input) +{ + if (input->type == LIBTERMINPUT_KEYPRESS) { + switch ((int)input->keypress.mods) { + case 0: + if (input->keypress.key == LIBTERMINPUT_TAB) { + } else if (input->keypress.key == LIBTERMINPUT_BACKTAB) backtab: { + } + break; + + case LIBTERMINPUT_SHIFT: + if (input->keypress.key == LIBTERMINPUT_TAB) + goto backtab; + break; + + case LIBTERMINPUT_CTRL: + case LIBTERMINPUT_CTRL | LIBTERMINPUT_SHIFT: + if (IS_KEY(input, 'Z')) { + restore_terminal(); + raise(SIGTSTP); + configure_terminal(); + redraw: + interrupt_deferred = 1; + term_resized = 1; + } else if (IS_KEY(input, 'L')) { + goto redraw; + } else if (IS_KEY(input, 'Q') || IS_KEY(input, 'C')) { + exiting = 1; + } + break; + + case LIBTERMINPUT_META: + if (input->keypress.key == LIBTERMINPUT_LEFT) { + automatic_left_pane_width = 0; + if (left_pane_width) { + left_pane_width -= 1U; + goto redraw; + } + } else if (input->keypress.key == LIBTERMINPUT_RIGHT) { + automatic_left_pane_width = 0; + if (term_width && left_pane_width < term_width - 1U) { + left_pane_width += 1U; + goto redraw; + } + } + break; + + case LIBTERMINPUT_META | LIBTERMINPUT_SHIFT: + case LIBTERMINPUT_META | LIBTERMINPUT_CTRL: + case LIBTERMINPUT_META | LIBTERMINPUT_CTRL | LIBTERMINPUT_SHIFT: + default: + break; + } + } +} + + +int +main(int argc, char *argv[]) +{ + int default_font_size_set = 0; + int min_font_size_set = 0; + int max_font_size_set = 0; + const char *font_family = "sans"; + size_t i, len; + struct libterminput_state input_ctx; + union libterminput_input input; + + ARGBEGIN { + case 'B': + listing = 1; + break; + case 'S': + listing = 0; + break; + case 'b': + use_bold = 1; + break; + case 'i': + use_italic = 1; + break; + case 'f': + font_family = ARG(); + break; + case 's': + parse_fontsize(ARG(), &default_font_size_set, &min_font_size_set, &max_font_size_set); + break; + default: + usage(); + } ARGEND; + + if (argc) + usage(); + + if (min_font_size_set && max_font_size_set) { + if (min_font_size > max_font_size) + eprintf("minimum font size cannot be greater than maximum font size"); + } else if (min_font_size_set) { + if (max_font_size < min_font_size) + max_font_size = min_font_size; + } else if (max_font_size_set) { + if (min_font_size > max_font_size) + min_font_size = max_font_size; + } + if (default_font_size_set) { + if (default_font_size < min_font_size) { + if (min_font_size_set) + eprintf("default font size cannot be less than the minimum font size"); + min_font_size = default_font_size; + } + if (default_font_size > max_font_size) { + if (max_font_size_set) + eprintf("default font size cannot be greater than the minimum font size"); + max_font_size = default_font_size; + } + } else { + if (default_font_size < min_font_size) + default_font_size = min_font_size; + else if (default_font_size > max_font_size) + default_font_size = max_font_size; + } + + if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) + eprintf(" and are expected to be terminal devices"); + + left_pane_width = strlen("No Block"); /* wider than "All" */ + for (i = 0; i < libcmap_block_list_size; i++) { + len = strlen(libcmap_block_list[i].name); + if (len > left_pane_width) + left_pane_width = len; + } + for (i = 0; i < libcmap_script_list_size; i++) { + len = strlen(libcmap_script_list[i].name); + if (len > left_pane_width) + left_pane_width = len; + } + + atexit(&cleanup); + libsimple_eprintf_preprint = &cleanup; + + setlocale(LC_ALL, ""); + + configure_terminal(); + subscribe_term_resize(); + memset(&input_ctx, 0, sizeof(input_ctx)); + libterminput_init(&input_ctx, STDIN_FILENO); + + goto beginning; + while (!exiting) { + if (interrupt_deferred) { + interrupt_deferred = 0; + goto interrupted; + } + switch (libterminput_read(STDIN_FILENO, &input, &input_ctx)) { + case 1: + handle_keyboard_input(&input); + break; + case 0: + exiting = 1; + break; + default: + if (errno != EINTR) + eprintf("libterminput :"); + interrupted: + if (term_resized) { + beginning: + term_resized = 0; + update_term_size(); + /* TODO redraw */ + } + break; + } + } + + restore_terminal(); + libterminput_destroy(&input_ctx); + return 0; +} diff --git a/common.h b/common.h new file mode 100644 index 0000000..7174f38 --- /dev/null +++ b/common.h @@ -0,0 +1,28 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef COMMON_H_ +#define COMMON_H_ + +#include +#include +#include + +#include +#include +#include +#include + + +#define COMMA , + + +#define IS_KEY(INPUT, KEY)\ + ((INPUT)->keypress.key == LIBTERMINPUT_SYMBOL &&\ + toupper((INPUT)->keypress.symbol[0]) == toupper((KEY)) &&\ + !(INPUT)->keypress.symbol[1]) + +#define IS_CTRL_KEY(INPUT, KEY)\ + (((INPUT)->keypress.mods & (enum libterminput_mod)~LIBTERMINPUT_SHIFT) == LIBTERMINPUT_CTRL &&\ + IS_KEY((INPUT), (KEY))) + + +#endif diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..222685b --- /dev/null +++ b/config.mk @@ -0,0 +1,8 @@ +PREFIX = /usr +MANPREFIX = $(PREFIX)/share/man + +CC = c99 + +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE +CFLAGS = +LDFLAGS = -lsimple -lterminput -lcmap -- cgit v1.3.1