diff options
| author | Mattias Andrée <m@maandree.se> | 2025-12-21 20:15:54 +0100 |
|---|---|---|
| committer | Mattias Andrée <m@maandree.se> | 2025-12-21 20:15:54 +0100 |
| commit | 3acf497ecf360459f314f8db3bda6ae564dcca0c (patch) | |
| tree | 576547c1249ff73dd283316e062110d31fa22667 | |
| download | cmap-3acf497ecf360459f314f8db3bda6ae564dcca0c.tar.gz cmap-3acf497ecf360459f314f8db3bda6ae564dcca0c.tar.bz2 cmap-3acf497ecf360459f314f8db3bda6ae564dcca0c.tar.xz | |
First commit
Signed-off-by: Mattias Andrée <m@maandree.se>
| -rw-r--r-- | .gitignore | 15 | ||||
| -rw-r--r-- | LICENSE | 15 | ||||
| -rw-r--r-- | Makefile | 37 | ||||
| -rw-r--r-- | README | 46 | ||||
| -rw-r--r-- | cmap.1 | 114 | ||||
| -rw-r--r-- | cmap.c | 390 | ||||
| -rw-r--r-- | common.h | 28 | ||||
| -rw-r--r-- | config.mk | 8 |
8 files changed, 653 insertions, 0 deletions
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 @@ -0,0 +1,15 @@ +ISC License + +© 2025 Mattias Andrée <m@maandree.se> + +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 @@ -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) @@ -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) @@ -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 <stdin>:"); + 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 <stdin> 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 <stdin> 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 <stdout> 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("<stdin> and <stdout> 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 <stdin>:"); + 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 <sys/epoll.h> +#include <locale.h> +#include <termios.h> + +#include <libsimple.h> +#include <libsimple-arg.h> +#include <libterminput.h> +#include <libcmap.h> + + +#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 |
