Commit 62d5d554 authored by Axel Wagner's avatar Axel Wagner

Imported Upstream version 0.0

parents
preload/shellex_preload.so
shellex
urxvt/shellex
Copyright © 2013 Axel Wagner
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Axel Wagner nor the
names of contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY Axel Wagner ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL Axel Wagner BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
TOPDIR=$(shell pwd)
include $(TOPDIR)/common.mk
ALL_TARGETS =
INSTALL_TARGETS =
CLEAN_TARGETS =
DISTCLEAN_TARGETS =
all: real-all
include preload/preload.mk
include shellex.mk
include urxvt/urxvt_shellex.mk
include conf.mk
include doc/man/man.mk
real-all: $(ALL_TARGETS)
install: $(INSTALL_TARGETS)
clean: $(CLEAN_TARGETS)
distclean: clean $(DISTCLEAN_TARGETS)
shellex - Shell-based launcher
==============================
`shellex` is supposed to be a dmenu-style launcher with a lot more features and
a lot simpler design. It launches a shell (currently `zsh`) and shows it in a
small terminal, wrapping every command with a little bit of extra magic
(redirecting stdout, stderr, disowning and closing the shell) to get more
typical launcher-behaviour.
This gives you a simple launcher with tab-completion and other shell-features,
configurable in shell.
I tried to do this a few years back, then using C and implementing the
terminal-operations myself. This turned out to be a very bad idea, it made the
design overly complex and the state I left it in had regular segfaults and was far
from working. After not much seem to happen in that direction, I decided to
start again, this time using `urxvt` to do the terminal-part, which turned out to
be really easy.
So, this is the early prototype. It is usable and should already work and be
usefull, but much is not working yet. I hope this time I will continue the work
for longer ;)
Architecture
============
`shellex` has three parts:
* [A small shell-script](shellex.in) that just starts a `urxvt` with some extra
parameters
* [An urxvt-extension](urxvt/shellex.in) that manages the terminal/displaying
part.
* [configfile](conf) that do all stuff relating to the functional behaviour
(Planned) Features
==================
Working:
* Launching Applications (yay)
* Commandline parameters
* Basic Tab-completion
* Starting on the right output (configurable, either the output containing the
currently focused window or the output containing the mousepointer)
* Dynamic resizing of the launcher-window e.g. for multiple lines of
suggestions for tab-completions (see [doc/autoresize.txt](doc/autoresize.txt))
Planned, but not Implemented yet:
* Buffering/showing some output, for errors etc. We have to think about some
magic way to determine, wether output is helpfull or the launcher should be
hidden immediately
* dmenu-like completion, typing part of a command still completing (maybe zsh
has sometething to do that?)
* .desktop-file integration
* Your ticket here
Installation
============
Just do
```sh
$ make
$ make install
```
Configuration
=============
Configuration of `shellex` has two parts: The first one are X-resources (which we will try to eliminate in the future):
Resource | Values | Default | Description
----------------- | -------------- | ------- | ---
URxvt.shellex.pos | pointer|focus | focus | If pointer, shellex shows the window on the window, the mousepointer is on, else it uses the output, where most of the currently focused window is.
URxvt.shellex.edge | bottom|top | top | On what screenedge to show shellex
The other are small shell-script-snippets. When starting, `shellex` will look
into `$HOME/.shellex` and into `/etc/shellex`. It will then source all the
snippets in either location. If there is an identically named file in both
directories, the one in your home will be preferred.
This makes for a pretty flexible configuration process: Usually there will be a
lot of snippets in `/usr/lib/shellex/conf`, which should be self-contained and
without a lot of side-effects. In `/etc/shellex` there then are some symlinks
to those snippets, making up the default-configuration on this system, together
with administrator-provided additional defaults. Whenever you don't want a
snippet form `/etc/shellex` to be used, just create a symlink of the same name
to `/dev/null` in `$HOME/.shellex`. If you want to create your own snippets,
just put them in `$HOME/.shellex` under a name not used yet and it will be
automatically sourced.
INSTALL=install
SED=sed
ifndef PREFIX
PREFIX=/usr
endif
ifndef SYSCONFDIR
ifeq ($(PREFIX),/usr)
SYSCONFDIR=/etc
else
SYSCONFDIR=$(PREFIX)/etc
endif
endif
LIBDIR ?= /lib
SHELLEX_CFLAGS = -std=c99
SHELLEX_CFLAGS += -Wall
SHELLEX_CFLAGS += -Wunused-value
sed_replace_vars := -e 's,@DESTDIR@,$(DESTDIR),g' \
-e 's,@PREFIX@,$(PREFIX),g' \
-e 's,@LIBDIR@,$(LIBDIR),g' \
-e 's,@SYSCONFDIR@,$(SYSCONFDIR),g'
V ?= 0
ifeq ($(V),0)
# Don't print command lines which are run
.SILENT:
endif
# always remake the following targets
.PHONY: install clean dist distclean
INSTALL_TARGETS += install-conf
default_confs := 10-autoexec 40-escape 40-setprompt 40-sigint 99-clear
install-conf:
$(INSTALL) -d -m 0755 $(DESTDIR)$(PREFIX)$(LIBDIR)/shellex/conf
for file in $(wildcard conf/*); \
do \
$(INSTALL) -m 0644 $${file} $(DESTDIR)$(PREFIX)$(LIBDIR)/shellex/conf/; \
done
$(INSTALL) -d -m 0755 $(DESTDIR)$(SYSCONFDIR)/shellex
for link in $(default_confs); \
do \
ln -s $(PREFIX)$(LIBDIR)/shellex/conf/$${link} $(DESTDIR)$(SYSCONFDIR)/shellex; \
done
# vim:ft=zsh
# Make zsh automatically execute a command, when enter is hit
zmodload zsh/regex
function accept-line () {
if [ "$BUFFER" -regex-match '^\s*$' ]
then
exit
fi
# We need to write the buffer to a temporary file to accomodate multiple
# commands (see https://github.com/Merovius/shellex/issues/11). We let the
# shell immediately remove the tempfile, so it's rather short-lived.
file=`tempfile`
echo "rm $file\n$BUFFER" > $file
# Execute the tempfile, then exit
BUFFER="zsh $file > /dev/null 2>&1 & disown; exit"
zle .accept-line
}
zle -N accept-line
# vim:ft=zsh
# Make zsh exit on escape
function _shellex_exit {
exit
}
zle -N _shellex_exit
bindkey '^[' _shellex_exit
# vim:ft=zsh
# Set the prompt
PROMPT="shellex> "
# vim:ft=zsh
# Make zsh exit on ^C
trap exit SIGINT
# vim:ft=zsh
clear
The process of automatically resizing the window to match the shell-output is
surprisingly complex. Normally the way the shell and terminal orchestrate
themselves to do the output is the following:
The terminal gets resized and does a TIOCSWINSZ ioctl on the pty-fd over which
the two processes communicate, giving the new dimenions. This prompts the
terminal to send the shell a SIGWINCH. The shell handles this by doing a
TIOCGWINSZ ioctl on the pty which returns the data the terminal gave.
zsh now uses this to determine, wether or not e.g. a tabcompletion-suggestion
fits on the terminal and if not, handles it differently. This is a problem for
shellex, because when is starting it's output, there is not enough space, for
the tabcompletion, so even if we immediately resize the terminalwindow, it will
be too late and the shell-output is screwed up.
We rectify this, by injecting a custom ioctl-function into urxvt via
LD_PRELOAD, which rewrites all TIOCSWINSZ-requests to have a constant size,
thus faking to the shell that there is more space available, then there
actually is. The actual number of rows is calculated on start of the urxvt and
put into an environment-variable.
With the current state, the window of shellex is able to grow automatically
exactly one time. This is, because zsh is spamming the output with '\n', if we
grow it always and we have yet to figure out why and how to stop that.
Shrinking (i.e. when the tab-completion vanishes again) is not implemented yet.
all:
$(MAKE) -C ../.. mans
clean:
$(MAKE) -C ../.. clean-mans
.PHONY: all clean
ifdef::doctype-manpage[]
ifdef::backend-docbook[]
[header]
template::[header-declarations]
<refentry>
<refmeta>
<refentrytitle>{mantitle}</refentrytitle>
<manvolnum>{manvolnum}</manvolnum>
<refmiscinfo class="source">shellex</refmiscinfo>
<refmiscinfo class="version">0.0</refmiscinfo>
<refmiscinfo class="manual">shellex Manual</refmiscinfo>
</refmeta>
<refnamediv>
<refname>{manname}</refname>
<refpurpose>{manpurpose}</refpurpose>
</refnamediv>
endif::backend-docbook[]
endif::doctype-manpage[]
DISTCLEAN_TARGETS += clean-mans
A2X = a2x
A2X_MAN_CALL = $(V_A2X)$(A2X) -f manpage --asciidoc-opts="-f doc/man/asciidoc.conf" $(A2X_FLAGS) $<
MANS = \
doc/man/shellex.1
mans: $(MANS)
%.1: %.man doc/man/asciidoc.conf
$(A2X_MAN_CALL)
%.man: %.man.in
$(SED) $(sed_replace_vars) $< > $@
clean-mans:
for file in $(basename $(MANS)); \
do \
rm -f $${file}.1 $${file}.man; \
done
shellex(1)
==========
Axel Wagner <mail@merovius.de>
v0.0, August 2013
== NAME
shellex - shell-based launcher
== SYNOPSIS
*shellex*
== DESCRIPTION
*shellex* is a shell-based launcher with a lot more features and a lot simpler
design. It launches a shell (currently zsh(1)) and shows it in a small
terminal (currently urxvt(1)), wrapping every command with a little bit of
extra magic (redirecting stdout, stderr, disowning and closing the shell) to
get more typical launcher-behaviour.
This gives you a simple launcher with tab-completion and other shell-features,
configurable in shell.
== RESOURCES
*shellex* uses two X-Resources at the monent, to manipulate its behaviour:
URxvt.shellex.pos::
If pointer, shellex shows the window on the window, the mousepointer is on. If
focus, it uses the output, where most of the currently focused window is.
Defaults to focus.
URxvt.shellex.edge::
On what screen edge to show the launcher (top or bottom). Defaults to top.
== CONFIGURATION
*shellex* configuration snippets can be found in *@PREFIX@@LIBDIR@/shellex/*.
On start, *shellex* looks into @SYSCONFDIR@/shellex for default-snippets to
source (usually this will be symlinks to *@PREFIX@@LIBDIR@/shellex/*) as well
as into *$HOME/.shellex/* for any user-configuration. If a file of the same
name exists in both locations, it will only use the one in *$HOME/.shellex/*.
To customize shellex, you can do the following things in *$HOME/.shellex/*:
1. Overwrite a default by creating a new snippet of the same name
2. Not include a default by creating a symlink to */dev/null* of the same same
3. Include an example-snippet not used by default, by creating a symlink to *@PREFIX@@LIBDIR@/shellex/snippet*
4. Write you own snippets with a currently unused name
To avoid naming-conflicts in the future, you should add a common suffix to all
your own snippets.
== AUTHORS
Axel Wagner <mail@merovius.de> and contributors
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdlib.h>
/* We can not take this from <sys/ioctl.h>, because it would define the
* ioctl-function itself
*/
struct winsize {
unsigned short ws_row;
unsigned short ws_col;
unsigned short ws_xpixel;
unsigned short ws_ypixel;
};
int ioctl (int d, int request, char *argp) {
static int (*orig_ioctl)(int, int, char *);
if (orig_ioctl == NULL) {
orig_ioctl = dlsym(RTLD_NEXT, "ioctl");
}
static int max_rows = -1;
if (max_rows < 0 ) {
char *str = getenv("SHELLEX_MAX_ROWS");
if (str != NULL) {
max_rows = atoi(str);
}
}
// We only care for TIOCSWINSZ ioctls
if (request != 0x5414) {
return orig_ioctl(d, request, argp);
}
struct winsize ws = *((struct winsize *)argp);
int fheight = ws.ws_ypixel / ws.ws_row;
if (max_rows < 0) {
ws.ws_row = 80;
ws.ws_ypixel += 80 * fheight;
} else {
ws.ws_row = max_rows;
ws.ws_ypixel += max_rows * fheight;
}
return orig_ioctl(d, request, (char *)&ws);
}
ALL_TARGETS += preload/shellex_preload.so
INSTALL_TARGETS += install-shellex_preload
CLEAN_TARGETS += clean-shellex_preload
SHELLEX_CFLAGS=-fPIC
SHELLEX_PRELOAD_LDFLAGS=-shared
preload/shellex_preload.so: preload/main.c
echo "[CC] $@"
$(CC) $(SHELLEX_CPPFLAGS) $(CPPFLAGS) $(SHELLEX_CFLAGS) $(CFLAGS) $(SHELLEX_PRELOAD_CFLAGS) $(LDFLAGS) $(SHELLEX_LDFLAGS) $(SHELLEX_PRELOAD_LDFLAGS) -o $@ $<
install-shellex_preload: preload/shellex_preload.so
echo "[INSTALL] $<"
$(INSTALL) -d -m 0755 $(DESTDIR)$(PREFIX)$(LIBDIR)/shellex
$(INSTALL) -m 0755 $< $(DESTDIR)$(PREFIX)$(LIBDIR)/shellex/
clean-shellex_preload:
echo "[CLEAN] shellex_preload"
rm -f preload/shellex_preload.so
#!/bin/sh
export LD_PRELOAD="@PREFIX@@LIBDIR@/shellex/shellex_preload.so"
exec urxvt -perl-lib @PREFIX@@LIBDIR@/shellex/urxvt -pe shellex -override-redirect -name shellex -e env -u LD_PRELOAD zsh -f
ALL_TARGETS += shellex
INSTALL_TARGETS += install-shellex
CLEAN_TARGETS += clean-shellex
SHELLEX_CFLAGS=-fPIC
SHELLEX_PRELOAD_LDFLAGS=-shared
shellex: shellex.in
echo "[SED] $@"
$(SED) $(sed_replace_vars) $< > $@
install-shellex: shellex
echo "[INSTALL] $<"
$(INSTALL) -d -m 0755 $(DESTDIR)$(PREFIX)/bin
$(INSTALL) -m 0755 shellex $(DESTDIR)$(PREFIX)/bin/
clean-shellex:
echo "[CLEAN] shellex"
rm -f shellex
# vim:ft=perl
#line 3
use X11::Protocol;
use File::Temp qw|tempfile|;
use File::Basename qw|basename|;
# The existing Randr-modules on CPAN seem to work only barely, so instead we
# just parse the output of xrandr -q. This is an uglyness, that should go away
# some time in the feature.
sub get_outputs {
my @outputs = ();
for my $line (qx(xrandr -q)) {
next unless $line =~ /\sconnected/;
my ($w, $h, $x, $y) = ($line =~ /(\d+)x(\d+)\+(\d+)\+(\d+)/);
push @outputs, { w => $w, h => $h, x => $x, y => $y };
}
return @outputs;
}
# This takes a list of outputs and looks up the one, the mouse pointer
# currently is on.
sub geometry_from_ptr {
my ($self) = @_;
my @outputs = get_outputs();
my $ptr = { $self->{X}->QueryPointer($self->DefaultRootWindow) };
for my $output (@outputs) {
if ($output->{x} <= $ptr->{root_x} && $ptr->{root_x} < $output->{x} + $output->{w}) {
$self->{x} = $output->{x};
if ($self->{bottom}) {
# The real y-coordinate will change during execution, when the window grows
$self->{y} = $output->{y} + $output->{h};
} else {
$self->{y} = $output->{y};
}
$self->{y} = $output->{y};
$self->{w} = $output->{w};
$self->{h} = $output->{h};
}
}
}
# This takes a list of outputs and looks up the one, that contains most of the
# window having the input focus currently
sub geometry_from_focus {
my ($self) = @_;
my @outputs = get_outputs();
# Look up the window that currently has the input focus
my ($focus, $revert) = $self->{X}->GetInputFocus();
my $geom = { $self->{X}->GetGeometry($focus) };
my ($fw, $fh) = ($geom->{width}, $geom->{height});
# The (x,y) coordinates we get are relative to the parent not the
# root-window. So we just translate the coordinates of the upper-left
# corner into the coordinate-system of the root-window
my (undef, undef, $fx, $fy) = $self->{X}->TranslateCoordinates($focus, $self->DefaultRootWindow, 0, 0);
# Returns the area (in pixel²) of the intersection of two rectangles.
# To understand how it works, best draw a picture.
my $intersection = sub {
my ($x, $y, $w, $h) = @_;
my $dx = $x + $w - $fx;
$dx = $dx > 0 ? $dx : 0;
$dx = $dx > $fw ? $fw : $dx;
my $dy = $y + $h - $fy;
$dy = $dy > 0 ? $dy : 0;
$dy = $dy > $fh ? $fh : $dy;
return $dx * $dy;
};
my $max_area = 0;
for my $output (@outputs) {
my $area = $intersection->($output->{x}, $output->{y}, $output->{w}, $output->{h});
if ($area >= $max_area) {
$max_area = $area;
$self->{x} = $output->{x};
if ($self->{bottom}) {
# The real y-coordinate will change during execution, when the window grows
$self->{y} = $output->{y} + $output->{h};
} else {
$self->{y} = $output->{y};
}
$self->{w} = $output->{w};
$self->{h} = $output->{h};
}
}
}
sub slurp {
open my $fh, '<', shift;
local $/;
<$fh>;
}
sub gen_conf {
my ($cfg, $cfgname) = tempfile("/tmp/shellex-XXXXXXXX", UNLINK => 0);
print $cfg "rm $cfgname\n";
my %fileset = ();
map { $fileset{basename($_)} = 1 } <@SYSCONFDIR@/shellex/*>;
map { $fileset{basename($_)} = 1 } <$ENV{HOME}/.shellex/*>;
my @files = sort keys %fileset;
for my $f (@files) {
if (-e "$ENV->{HOME}/.shellex/$f") {
print $cfg slurp("$ENV{HOME}/.shellex/$f");
} else {
print $cfg slurp("@SYSCONFDIR@/shellex/$f");
}
}
close($cfg);
return $cfgname;
}
# This hook is run when the extension is first initialized, before any windows
# are created or mapped. There is not much work we can do here.
sub on_init {
my ($self) = @_;
$self->{X} = X11::Protocol->new($self->display_id);
# Some reasonably sane values in case all our methods to determine a
# geometry fails.
$self->{x} = 0;
$self->{y} = 0;
$self->{w} = 1024;
$self->{h} = 768;
();
}
# This hook is run after the window is created, but before it is mapped, so
# this is the place to set the geometry to what we want
sub on_start {
my ($self) = @_;
if ($self->x_resource("%.edge") eq 'bottom') {
print "position should be at the bottom\n";
$self->{bottom} = 1;
$self->{y} = $self->{h};
} else {
print "position should be at the top\n";
}
if ($self->x_resource("%.pos") eq 'pointer') {
print "Getting shellex-position from pointer\n";
$self->geometry_from_ptr();
} else {
print "Getting shellex-position from focused window\n";
$self->geometry_from_focus();
}
# This environment variable is used by the LD_PRELOAD ioctl-override to
# determine the values to send to the shell
$ENV{SHELLEX_MAX_HEIGHT} = int($self->{h} / $self->fheight);
# Our initial position is different, if we have to be at the bottom
if ($self->{bottom}) {
$self->XMoveResizeWindow($self->parent, $self->{x}, $self->{y} - (2 + $self->fheight), $self->{w}, 2+$self->fheight);
} else {
$self->XMoveResizeWindow($self->parent, $self->{x}, $self->{y}, $self->{w}, 2+$self->fheight);
}
my $cfg = gen_conf();
$self->tt_write($self->locale_encode(". $cfg\n"));
();
}
# This hook is run every time a line was changed. We do some resizing here,
# because this catches most cases where we would want to shrink our window.
sub on_line_update {
my ($self, $row) = @_;
print "line_update(row = $row)\n";
# Determine the last row, that is not empty.
# TODO: Does this work as intended, if there is an empty line in the
# middle?
my $nrow = 0;
for my $i ($self->top_row .. $self->nrow-1) {
if ($self->ROW_l($i) > 0) {
print "row $i is " . $self->ROW_l($i) . "\n";
$nrow++;
}
}
$nrow = $nrow > 0 ? $nrow : 1;
print "resizing to $nrow\n";
# If the window is supposed to be at the bottom, we have to move the
# window up a little bit
my $y = $self->{y};
if ($self->{bottom}) {
$y -= 2+$nrow*$self->fheight;
}
$self->cmd_parse("\e[8;$nrow;t\e[3;$self->{x};${y}t");
();
}
# This hook is run every time before there is text output. We resize here,
# immediately before new lines would be added, which would create scrolling
sub on_add_lines {
my ($self, $string) = @_;
my $str = $string;
$str =~ s/\n/\\n/g;
$str =~ s/\r/\\r/g;
print "add_lines(string = \"$str\")\n";
my $nl = ($string =~ tr/\n//);
if ($nl > 0) {
my $nrow = 0;
for my $i ($self->top_row .. $self->nrow-1) {
if ($self->ROW_l($i) > 0) {
print "row $i is " . $self->ROW_l($i) . "\n";
$nrow++;
}
}
$nrow = $nrow > 0 ? $nrow : 1;
print "resizing to $nrow + $nl\n";
$nrow += $nl;
# If the window is supposed to be at the bottom, we have to move the
# window up a little bit
my $y = $self->{y};
if ($self->{bottom}) {
$y -= 2+$nrow*$self->fheight;
}
$self->cmd_parse("\e[8;$nrow;t\e[3;$self->{x};${y}t");
}
();
}
# Just for debugging
sub on_size_change {
my ($self, $nw, $nh) = @_;
print "size_change($nw, $nh)\n";
();
}
sub on_view_change {
my ($self, $offset) = @_;
print "view_change(offset = $offset)\n";
();
}
sub on_scroll_back {
my ($self, $lines, $saved) = @_;
print "scroll_back(lines = $lines, saved = $saved)\n";
();
}
# This hook is run directly after the window was mapped (= displayed on
# screen). We grab the keyboard here.
sub on_map_notify {
my ($self, $ev) = @_;
$self->grab($self->{data}{event}{time}, 1);
$self->allow_events_async;
$self->focus_in;
();
}
ALL_TARGETS += urxvt/shellex
INSTALL_TARGETS += install-urxvt_shellex
CLEAN_TARGETS += clean-urxvt_shellex
urxvt/shellex: urxvt/shellex.in
echo "[SED] $@"
$(SED) $(sed_replace_vars) $< > $@
install-urxvt_shellex: urxvt/shellex
echo "[INSTALL] $<"
$(INSTALL) -d -m 0755 $(DESTDIR)$(PREFIX)$(LIBDIR)/shellex/urxvt
$(INSTALL) -m 0644 urxvt/shellex $(DESTDIR)$(PREFIX)$(LIBDIR)/shellex/urxvt/
clean-urxvt_shellex:
echo "[CLEAN] urxvt/shellex"
rm -f urxvt/shellex
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment