flbng

flash bang - show full screen centered text in terminal
git clone git://git.konyahin.xyz/flbng
Log | Files | Refs | LICENSE

commit 101a421d54e0a443b5497995e30073cb0dc14077
Author: Anton Konyahin <me@konyahin.xyz>
Date:   Sun, 10 Mar 2024 17:13:47 +0300

initial commit

Diffstat:
ALICENSE | 21+++++++++++++++++++++
AMakefile | 39+++++++++++++++++++++++++++++++++++++++
Amain.c | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest.sh | 35+++++++++++++++++++++++++++++++++++
4 files changed, 260 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT/X Consortium License + +© 2024 Anton Konyahin me@konyahin.xyz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,39 @@ +.POSIX: +.SUFFIXES: +.PHONY: install uninstall clean + +BIN = flbng +CC = cc +CFLAGS = -Wall -Wextra -Werror -Os +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +OBJ = main.o + +all: $(BIN) + +$(BIN): $(OBJ) + $(CC) $(LDFLAGS) -o $(BIN) $(OBJ) $(LDLIBS) + +run: $(BIN) + ./$(BIN) + +test: $(BIN) + ./test.sh + +install: $(BIN) + mkdir -p $(DESTDIR)$(PREFIX)/bin + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + install -m 775 $(BIN) $(DESTDIR)$(PREFIX)/bin/ + install -m 644 $(BIN).1 $(DESTDIR)$(MANPREFIX)/man1/ + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/$(BIN) + rm -f $(DESTDIR)$(MANPREFIX)/man1/$(BIN).1 + +clean: + rm -f $(BIN) *.o + +.SUFFIXES: .c .o +.c.o: + $(CC) $(CFLAGS) -c $< diff --git a/main.c b/main.c @@ -0,0 +1,165 @@ +#include <ctype.h> +#include <err.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <termios.h> +#include <unistd.h> + +struct screen_size { + unsigned short width; + unsigned short height; +}; + +struct data { + char **data; + size_t lines; + size_t max_width; +}; + +// global variable for terminal state restoring +struct termios orig_term; + +void +restoreTerminal() { + // show cursor + puts("\e[?25h"); + if (tcsetattr(STDOUT_FILENO, TCSAFLUSH, &orig_term) < 0) + err(1, "can't restore terminal state"); + // clear screen + puts("\x1b[2J"); +} + +void +prepareTerminal() { + if (tcgetattr(STDOUT_FILENO, &orig_term) < 0) + err(1, "can't get terminal attributes"); + + atexit(restoreTerminal); + + // havely inspired by https://github.com/antirez/kilo/blob/master/kilo.c + struct termios raw = orig_term; + raw.c_iflag &= + (unsigned) ~(BRKINT | INPCK | ISTRIP | ICRNL | IXON); + raw.c_oflag &= (unsigned) ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= + (unsigned) ~(ECHO | ICANON); + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 1; + + if (tcsetattr(STDOUT_FILENO, TCSAFLUSH, &raw) < 0) + err(1, "can't set terminal attributes"); + + // hide cursor + puts("\e[?25l"); + // clear screen + puts("\x1b[2J"); +} + +void +processInput() { + // read from /dev/tty, because standart input + // will be redirected + int input = open("/dev/tty", O_RDONLY); + char c = '\0'; + while (1) { + ssize_t read_s = read(input, &c, 1); + if (read_s < 0) + err(1, "can't read from tty"); + if (read_s == 0) + continue; + break; + } +} + +struct screen_size +getScreenSize() { + struct winsize size; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &size); + return (struct screen_size) { + .width = size.ws_col, + .height = size.ws_row, + }; +} + +struct data +getInput(struct screen_size s) { + struct data text; + text.data = calloc(sizeof(char*), s.height); + text.lines = 0; + text.max_width = 0; + + while (1) { + char *line = NULL; + ssize_t len = 0; + size_t buf_len = 0; + + if ((len = getline(&line, &buf_len, stdin)) < 0) + break; + + // we can reach EOF here, if there is no delimiter on the last line + // if there is delimiter - we should remove it + if (!feof(stdin)) { + len -= 1; + line[len] = '\0'; + } + + if ((size_t) len > text.max_width) { + text.max_width = len; + if (len > s.width) + errx(1, "too long line for this terminal, should be shorter than %d", s.width); + } + text.data[text.lines] = line; + text.lines += 1; + if (text.lines > s.height) + errx(1, "too much lines for this terminal, should be less than %d", s.height); + } + + if (ferror(stdin)) + err(1, "can't read line from stdin"); + + if (text.lines == 0) + err(1, "empty input"); + + return text; +} + +int +main(void) { + // TODO: -h for help + + struct screen_size s = getScreenSize(); + struct data text = getInput(s); + + size_t start_position = (s.height - text.lines) / 2; + int margin = (int) ((s.width - text.max_width) / 2); + + prepareTerminal(); + + for (size_t i = 0; i < start_position; i++) + printf("\r\n"); + + for (size_t i = 0; i < text.lines; i++) { + if (margin) + printf("%*s", margin, " "); + printf("%s", text.data[i]); + if (i != text.lines - 1) + printf("\r\n"); + } + + for (size_t i = start_position + text.lines; i < s.height; i++) + printf("\r\n"); + + fflush(stdout); + + processInput(); + + for (size_t i = 0; i < text.lines; i++) + free(text.data[i]); + free(text.data); + + return 0; +} diff --git a/test.sh b/test.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env sh + +# no new line at the end of text +printf "first normal line\nno new line at the end" | ./flbng + +# ascii art +figlet flash bang test | ./flbng + +# random fortune text +/usr/games/fortune | ./flbng + +# as much lines as terminal height +lines() { + for i in $(seq $(tput lines)) + do echo "$i" + done +} +lines | ./flbng + +# as much chars as terinal widht +chars () { + for i in $(seq $(tput cols)) + do printf "*" + done +} +chars | ./flbng + +./flbng <<EOF +Some errors check. Should be: + flbng: too much lines for this terminal, should be less than X + flbng: too long line for this terminal, should be shorter than X +EOF + +echo "0\n$(lines)" | ./flbng +echo "*$(chars)" | ./flbng