The use of mkstemp'

This commit is contained in:
elbachir-one
2024-07-03 22:44:40 +01:00
committed by Łukasz Pankowski
parent 6e97047474
commit ef88bcb749
7 changed files with 579 additions and 0 deletions

View File

@@ -38,6 +38,8 @@ install: st
mkdir -p $(DESTDIR)$(PREFIX)/bin
cp -f st $(DESTDIR)$(PREFIX)/bin
chmod 755 $(DESTDIR)$(PREFIX)/bin/st
cp -f st-autocomplete $(DESTDIR)$(PREFIX)/bin
chmod 755 $(DESTDIR)$(PREFIX)/bin/st-autocomplete
mkdir -p $(DESTDIR)$(MANPREFIX)/man1
sed "s/VERSION/$(VERSION)/g" < st.1 > $(DESTDIR)$(MANPREFIX)/man1/st.1
chmod 644 $(DESTDIR)$(MANPREFIX)/man1/st.1
@@ -46,6 +48,7 @@ install: st
uninstall:
rm -f $(DESTDIR)$(PREFIX)/bin/st
rm -f $(DESTDIR)$(PREFIX)/bin/st-autocomplete
rm -f $(DESTDIR)$(MANPREFIX)/man1/st.1
.PHONY: all clean dist install uninstall

16
autocomplete.h Normal file
View File

@@ -0,0 +1,16 @@
# ifndef __ST_AUTOCOMPLETE_H
# define __ST_AUTOCOMPLETE_H
enum {
ACMPL_DEACTIVATE,
ACMPL_WORD,
ACMPL_WWORD,
ACMPL_FUZZY_WORD,
ACMPL_FUZZY_WWORD,
ACMPL_FUZZY,
ACMPL_SUFFIX,
ACMPL_SURROUND,
ACMPL_UNDO,
};
# endif // __ST_AUTOCOMPLETE_H

View File

@@ -170,6 +170,8 @@ static unsigned int defaultattr = 11;
*/
static uint forcemousemod = ShiftMask;
#include "autocomplete.h"
/*
* Internal mouse shortcuts.
* Beware that overloading Button1 will disable the selection.
@@ -187,6 +189,8 @@ static MouseShortcut mshortcuts[] = {
#define MODKEY Mod1Mask
#define TERMMOD (ControlMask|ShiftMask)
#define ACMPL_MOD ControlMask|Mod1Mask
static Shortcut shortcuts[] = {
/* mask keysym function argument */
{ XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} },
@@ -201,6 +205,14 @@ static Shortcut shortcuts[] = {
{ TERMMOD, XK_Y, selpaste, {.i = 0} },
{ ShiftMask, XK_Insert, selpaste, {.i = 0} },
{ TERMMOD, XK_Num_Lock, numlock, {.i = 0} },
{ ACMPL_MOD, XK_slash, autocomplete, { .i = ACMPL_WORD } },
{ ACMPL_MOD, XK_period, autocomplete, { .i = ACMPL_FUZZY_WORD } },
{ ACMPL_MOD, XK_comma, autocomplete, { .i = ACMPL_FUZZY } },
{ ACMPL_MOD, XK_apostrophe, autocomplete, { .i = ACMPL_SUFFIX } },
{ ACMPL_MOD, XK_semicolon, autocomplete, { .i = ACMPL_SURROUND } },
{ ACMPL_MOD, XK_bracketright,autocomplete, { .i = ACMPL_WWORD } },
{ ACMPL_MOD, XK_bracketleft, autocomplete, { .i = ACMPL_FUZZY_WWORD } },
{ ACMPL_MOD, XK_equal, autocomplete, { .i = ACMPL_UNDO } },
};
/*

310
st-autocomplete Normal file
View File

@@ -0,0 +1,310 @@
#!/usr/bin/perl
#########################################################################
# Copyright (C) 2012-2017 Wojciech Siewierski #
# #
# 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/>. #
#########################################################################
my ($cmd, $cursor_row, $cursor_column) = @ARGV;
my $lines = [];
my $lines1 = [];
my $last_line = -1;
my $lines_before_cursor = 0;
while (<stdin>)
{
$last_line++;
s/[^[:print:]]/?/g;
if ($last_line < $cursor_row)
{
unshift @{$lines1}, $_;
$lines_before_cursor++;
}
else
{
unshift @{$lines}, $_;
}
}
foreach (@{$lines1})
{
unshift @{$lines}, $_;
}
my $cursor_row_in = $cursor_row;
$cursor_row = $last_line;
$self = {};
# A reference to a function that transforms the completed word
# into a regex matching the completions. Usually generated by
# generate_matcher().
#
# For example
# $fun = generate_matcher(".*");
# $fun->("foo");
# would return "f.*o.*o"
#
# In other words, indirectly decides which characters can
# appear in the completion.
my $matcher;
# A regular expression matching a character before each match.
# For example, it you want to match the text after a
# whitespace, set it to "\s".
my $char_class_before;
# A regular expression matching every character in the entered
# text that will be used to find matching completions. Usually
# "\w" or similar.
my $char_class_to_complete;
# A regular expression matching every allowed last character
# of the completion (uses greedy matching).
my $char_class_at_end;
if ($cmd eq 'word-complete') {
# Basic word completion. Completes the current word
# without any special matching.
$char_class_before = '[^-\w]';
$matcher = sub { quotemeta shift }; # identity
$char_class_at_end = '[-\w]';
$char_class_to_complete = '[-\w]';
} elsif ($cmd eq 'WORD-complete') {
# The same as above but in the Vim meaning of a "WORD" --
# whitespace delimited.
$char_class_before = '\s';
$matcher = sub { quotemeta shift };
$char_class_at_end = '\S';
$char_class_to_complete = '\S';
} elsif ($cmd eq 'fuzzy-word-complete' ||
$cmd eq 'skeleton-word-complete') {
# Fuzzy completion of the current word.
$char_class_before = '[^-\w]';
$matcher = generate_matcher('[-\w]*');
$char_class_at_end = '[-\w]';
$char_class_to_complete = '[-\w]';
} elsif ($cmd eq 'fuzzy-WORD-complete') {
# Fuzzy completion of the current WORD.
$char_class_before = '\s';
$matcher = generate_matcher('\S*');
$char_class_at_end = '\S';
$char_class_to_complete = '\S';
} elsif ($cmd eq 'fuzzy-complete' ||
$cmd eq 'skeleton-complete') {
# Fuzzy completion of an arbitrary text.
$char_class_before = '\W';
$matcher = generate_matcher('.*?');
$char_class_at_end = '\w';
$char_class_to_complete = '\S';
} elsif ($cmd eq 'suffix-complete') {
# Fuzzy completion of an completing suffixes, like
# completing test=hello from /blah/hello.
$char_class_before = '\S';
$matcher = generate_matcher('\S*');
$char_class_at_end = '\S';
$char_class_to_complete = '\S';
} elsif ($cmd eq 'surround-complete') {
# Completing contents of quotes and braces.
# Here we are using three named groups: s, b, p for quotes, braces
# and parenthesis.
$char_class_before = '((?<q>["\'`])|(?<b>\[)|(?<p>\())';
$matcher = generate_matcher('.*?');
# Here we match text till enclosing pair, using perl conditionals in
# regexps (?(condition)yes-expression|no-expression).
# \0 is used to hack concatenation with '*' later in the code.
$char_class_at_end = '.*?(.(?=(?(<b>)\]|((?(<p>)\)|\g{q})))))\0';
$char_class_to_complete = '\S';
}
# use the last used word or read the word behind the cursor
my $word_to_complete = read_word_at_coord($self, $cursor_row, $cursor_column,
$char_class_to_complete);
print stdout "$word_to_complete\n";
if ($word_to_complete) {
while (1) {
# ignore the completed word itself
$self->{already_completed}{$word_to_complete} = 1;
# continue the last search or start from the current row
my $completion = find_match($self,
$word_to_complete,
$self->{next_row} // $cursor_row,
$matcher->($word_to_complete),
$char_class_before,
$char_class_at_end);
if ($completion) {
print stdout $completion."\n".join ("\n", @{$self->{highlight}})."\n";
}
else {
last;
}
}
}
######################################################################
sub highlight_match {
my ($self, $linenum, $completion) = @_;
# clear_highlight($self);
my $line = @{$lines}[$linenum];
my $re = quotemeta $completion;
$line =~ /$re/;
my $beg = $-[0];
my $end = $+[0];
if ($linenum >= $lines_before_cursor)
{
$lline = $last_line - $lines_before_cursor;
$linenum -= $lines_before_cursor;
$linenum = $lline - $linenum;
$linenum += $lines_before_cursor;
}
$self->{highlight} = [$linenum, $beg, $end];
}
######################################################################
sub read_word_at_coord {
my ($self, $row, $col, $char_class) = @_;
$_ = substr(@{$lines} [$row], 0, $col); # get the current line up to the cursor...
s/.*?($char_class*)$/$1/; # ...and read the last word from it
return $_;
}
######################################################################
# Returns a function that takes a string and returns that string with
# this function's argument inserted between its every two characters.
# The resulting string is used as a regular expression matching the
# completion candidates.
sub generate_matcher {
my $regex_between = shift;
sub {
$_ = shift;
# sorry for this lispy code, I couldn't resist ;)
(join "$regex_between",
(map quotemeta,
(split //)))
}
}
######################################################################
# Checks whether the completion found by find_match() was already
# found and if it was, calls find_match() again to find the next
# completion.
#
# Takes all the arguments that find_match() would take, to make a
# mutually recursive call.
sub skip_duplicates {
my ($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end) = @_;
my $completion;
if ($current_row <= $lines_before_cursor)
{
$completion = shift @{$self->{matches_in_row}}; # get the leftmost one
}
else
{
$completion = pop @{$self->{matches_in_row}}; # get the leftmost one
}
# check for duplicates
if (exists $self->{already_completed}{$completion}) {
# skip this completion
return find_match(@_);
} else {
$self->{already_completed}{$completion} = 1;
highlight_match($self,
$self->{next_row}+1,
$completion);
return $completion;
}
}
######################################################################
# Finds the next matching completion in the row current row or above
# while skipping duplicates using skip_duplicates().
sub find_match {
my ($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end) = @_;
$self->{matches_in_row} //= [];
# cycle through all the matches in the current row if not starting a new search
if (@{$self->{matches_in_row}}) {
return skip_duplicates($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end);
}
my $i;
# search through all the rows starting with current one or one above the last checked
for ($i = $current_row; $i >= 0; --$i) {
my $line = @{$lines}[$i]; # get the line of text from the row
# if ($i == $cursor_row) {
# $line = substr $line, 0, $cursor_column;
# }
$_ = $line;
# find all the matches in the current line
my $match;
push @{$self->{matches_in_row}}, $+{match} while ($_, $match) = /
(.*${char_class_before})
(?<match>
${regexp}
${char_class_at_end}*
)
/ix;
# corner case: match at the very beginning of line
push @{$self->{matches_in_row}}, $+{match} if $line =~ /^(${char_class_before}){0}(?<match>$regexp$char_class_at_end*)/i;
if (@{$self->{matches_in_row}}) {
# remember which row should be searched next
$self->{next_row} = --$i;
# arguments needed for find_match() mutual recursion
return skip_duplicates($self, $word_to_match, $i, $regexp, $char_class_before, $char_class_at_end);
}
}
# # no more possible completions, revert to the original word
# undo_completion($self) if $i < 0;
return undef;
}

227
st.c
View File

@@ -17,6 +17,7 @@
#include <unistd.h>
#include <wchar.h>
#include "autocomplete.h"
#include "st.h"
#include "win.h"
@@ -2580,6 +2581,8 @@ tresize(int col, int row)
return;
}
autocomplete ((const Arg []) { ACMPL_DEACTIVATE });
/*
* slide screen to keep cursor where we expect it -
* tscrollup would work here, but we can optimize to
@@ -2699,3 +2702,227 @@ redraw(void)
tfulldirt();
draw();
}
void autocomplete (const Arg *arg) {
static _Bool active = 0;
int acmpl_cmdindex = arg->i;
static int acmpl_cmdindex_prev;
if (active == 0)
acmpl_cmdindex_prev = acmpl_cmdindex;
static const char * const acmpl_cmd[] = {
[ACMPL_DEACTIVATE] = "__DEACTIVATE__",
[ACMPL_WORD] = "word-complete",
[ACMPL_WWORD] = "WORD-complete",
[ACMPL_FUZZY_WORD] = "fuzzy-word-complete",
[ACMPL_FUZZY_WWORD] = "fuzzy-WORD-complete",
[ACMPL_FUZZY] = "fuzzy-complete",
[ACMPL_SUFFIX] = "suffix-complete",
[ACMPL_SURROUND] = "surround-complete",
[ACMPL_UNDO] = "__UNDO__",
};
static FILE *acmpl_exec = NULL;
static int acmpl_status;
static char *stbuffile;
static char *target = NULL;
static size_t targetlen;
static char *completion = NULL;
static size_t complen_prev = 0;
static int cx, cy;
if (acmpl_cmdindex == ACMPL_DEACTIVATE) {
if (active) {
active = 0;
pclose(acmpl_exec);
unlink(stbuffile);
free(stbuffile);
stbuffile = NULL;
if (complen_prev) {
selclear();
complen_prev = 0;
}
}
return;
}
if (acmpl_cmdindex == ACMPL_UNDO) {
if (active) {
active = 0;
pclose(acmpl_exec);
unlink(stbuffile);
free(stbuffile);
stbuffile = NULL;
if (complen_prev) {
selclear();
for (size_t i = 0; i < complen_prev; i++)
ttywrite((char[]) {'\b'}, 1, 1);
complen_prev = 0;
ttywrite(target, targetlen, 0);
}
}
return;
}
if (acmpl_cmdindex != acmpl_cmdindex_prev) {
if (active) {
acmpl_cmdindex_prev = acmpl_cmdindex;
goto acmpl_begin;
}
}
if (active == 0) {
acmpl_cmdindex_prev = acmpl_cmdindex;
cx = term.c.x;
cy = term.c.y;
char filename[] = "/tmp/st-autocomplete-XXXXXX";
int fd = mkstemp(filename);
if (fd == -1) {
perror("mkstemp");
return;
}
stbuffile = strdup(filename);
FILE *stbuf = fdopen(fd, "w");
if (!stbuf) {
perror("fdopen");
close(fd);
unlink(stbuffile);
free(stbuffile);
stbuffile = NULL;
return;
}
char *stbufline = malloc(term.col + 2);
if (!stbufline) {
perror("malloc");
fclose(stbuf);
unlink(stbuffile);
free(stbuffile);
stbuffile = NULL;
return;
}
int cxp = 0;
for (size_t y = 0; y < term.row; y++) {
if (y == term.c.y) cx += cxp * term.col;
size_t x = 0;
for (; x < term.col; x++)
utf8encode(term.line[y][x].u, stbufline + x);
if (term.line[y][x - 1].mode & ATTR_WRAP) {
x--;
if (y <= term.c.y) cy--;
cxp++;
} else {
stbufline[x] = '\n';
cxp = 0;
}
stbufline[x + 1] = 0;
fputs(stbufline, stbuf);
}
free(stbufline);
fclose(stbuf);
acmpl_begin:
target = malloc(term.col + 1);
completion = malloc(term.col + 1);
if (!target || !completion) {
perror("malloc");
free(target);
free(completion);
unlink(stbuffile);
free(stbuffile);
stbuffile = NULL;
return;
}
char acmpl[1500];
snprintf(acmpl, sizeof(acmpl),
"cat %s | st-autocomplete %s %d %d",
stbuffile, acmpl_cmd[acmpl_cmdindex], cy, cx);
acmpl_exec = popen(acmpl, "r");
if (!acmpl_exec) {
perror("popen");
free(target);
free(completion);
unlink(stbuffile);
free(stbuffile);
stbuffile = NULL;
return;
}
if (fscanf(acmpl_exec, "%s\n", target) != 1) {
perror("fscanf");
pclose(acmpl_exec);
free(target);
free(completion);
unlink(stbuffile);
free(stbuffile);
stbuffile = NULL;
return;
}
targetlen = strlen(target);
}
unsigned line, beg, end;
acmpl_status = fscanf(acmpl_exec, "%[^\n]\n%u\n%u\n%u\n", completion, &line, &beg, &end);
if (acmpl_status == EOF) {
if (active == 0) {
pclose(acmpl_exec);
free(target);
free(completion);
unlink(stbuffile);
free(stbuffile);
stbuffile = NULL;
return;
}
active = 0;
pclose(acmpl_exec);
ttywrite(target, targetlen, 0);
goto acmpl_begin;
}
active = 1;
if (complen_prev == 0) {
for (size_t i = 0; i < targetlen; i++)
ttywrite((char[]) {'\b'}, 1, 1);
} else {
selclear();
for (size_t i = 0; i < complen_prev; i++)
ttywrite((char[]) {'\b'}, 1, 1);
complen_prev = 0;
}
complen_prev = strlen(completion);
ttywrite(completion, complen_prev, 0);
if (line == cy && beg > cx) {
beg += complen_prev - targetlen;
end += complen_prev - targetlen;
}
end--;
int wl = 0;
int tl = line;
for (int l = 0; l < tl; l++)
if (term.line[l][term.col - 1].mode & ATTR_WRAP) {
wl++;
tl++;
}
selstart(beg % term.col, line + wl + beg / term.col, 0);
selextend(end % term.col, line + wl + end / term.col, 1, 0);
xsetsel(getsel());
}

2
st.h
View File

@@ -77,6 +77,8 @@ typedef union {
const char *s;
} Arg;
void autocomplete (const Arg *);
void die(const char *, ...);
void redraw(void);
void draw(void);

9
x.c
View File

@@ -1862,11 +1862,20 @@ kpress(XEvent *ev)
/* 1. shortcuts */
for (bp = shortcuts; bp < shortcuts + LEN(shortcuts); bp++) {
if (ksym == bp->keysym && match(bp->mod, e->state)) {
if (bp -> func != autocomplete)
autocomplete ((const Arg []) { ACMPL_DEACTIVATE });
bp->func(&(bp->arg));
return;
}
}
if (!(
len == 0 &&
e -> state & ~ignoremod // ACMPL_ISSUE: I'm not sure that this is the right way
| ACMPL_MOD == ACMPL_MOD
))
autocomplete ((const Arg []) { ACMPL_DEACTIVATE });
/* 2. custom keys from config.h */
if ((customkey = kmap(ksym, e->state))) {
ttywrite(customkey, strlen(customkey), 1);