Initial commit

This commit is contained in:
Přemysl Eric Janouch 2017-06-29 02:50:39 +02:00
commit a55fc17f31
Signed by: p
GPG Key ID: B715679E3A361BE6
5 changed files with 592 additions and 0 deletions

46
CMakeLists.txt Normal file
View File

@ -0,0 +1,46 @@
# target_compile_features has been introduced in that version
cmake_minimum_required (VERSION 3.1.0)
project (sdn CXX)
set (version 0.1)
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
endif ()
# Since we use a language with slow compilers, let's at least use a fast linker
execute_process (COMMAND ${CMAKE_CXX_COMPILER} -fuse-ld=gold -Wl,--version
ERROR_QUIET OUTPUT_VARIABLE ld_version)
if ("${ld_version}" MATCHES "GNU gold")
set (CMAKE_EXE_LINKER_FLAGS "-fuse-ld=gold ${CMAKE_EXE_LINKER_FLAGS}")
endif ()
find_package (PkgConfig REQUIRED)
pkg_check_modules (NCURSESW QUIET ncursesw)
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.cpp)
target_include_directories (${PROJECT_NAME} PUBLIC ${NCURSESW_INCLUDE_DIRS})
target_link_libraries (${PROJECT_NAME} PUBLIC ${NCURSESW_LIBRARIES})
target_compile_features (${PROJECT_NAME} PUBLIC cxx_std_14)
target_compile_definitions (${PROJECT_NAME} PUBLIC
-DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${version}\")
include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator")
set (CPACK_PACKAGE_VENDOR "Premysl Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Janouch <p.janouch@gmail.com>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_PACKAGE_VERSION ${version})
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${version}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${version}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${version}")
set (CPACK_SET_DESTDIR TRUE)
include (CPack)

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright (c) 2017, Přemysl Janouch <p.janouch@gmail.com>
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.

12
Makefile Normal file
View File

@ -0,0 +1,12 @@
SHELL = /bin/sh
CXXFLAGS = -g -std=c++14 -Wall -Wextra -pedantic -static-libstdc++
all: sdn
%: %.cpp CMakeLists.txt
$(CXX) $(CXXFLAGS) $< -o $@ `pkg-config --libs --cflags ncursesw` \
`sed -ne 's/^project (\([^ )]*\).*/-DPROJECT_NAME="\1"/p' \
-e 's/^set (version \([^ )]*\).*/-DPROJECT_VERSION="\1"/p' CMakeLists.txt`
clean:
rm -f sdn
.PHONY: all clean

75
README.adoc Normal file
View File

@ -0,0 +1,75 @@
sdn
===
:compact-option:
'sdn' is a simple directory navigator that you can invoke while editing shell
commands. It enables you to:
* take a quick peek at directory contents without running `ls`
* browse the filesystem without all the mess that Midnight Commander does:
there's no need to create a subshell in a new pty. The current command line
can be simply forwarded if it is to be edited. What's more, it will always
be obvious whether the navigator is running.
Development has just started and the only supported platform is Linux.
I wanted to try a different, simpler approach here.
Building
--------
Build dependencies: CMake and/or make, a C++14 compiler, pkg-config +
Runtime dependencies: ncursesw
$ git clone https://github.com/pjanouch/sdn.git
$ mkdir sdn/build
$ cd sdn/build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug
$ make
To install the application, you can do either the usual:
# make install
Or you can try telling CMake to make a package for you. For Debian it is:
$ cpack -G DEB
# dpkg -i sdn-*.deb
There is also a Makefile you can use to quickly build a binary to be copied
into the PATH of any machine you want to have 'sdn' on.
zsh
---
To start using this navigator, put the following in your .zshrc:
....
navigate () {
# ... possibly zle-line-init
eval `navigator`
[ -z "$cd" ] || cd "$cd"
[ -z "$insert" ] || LBUFFER="$LBUFFER$insert "
zle reset-prompt
# ... possibly zle-line-finish
}
zle -N navigate
bindkey '\eo' navigate
....
As far as I'm aware, bash cannot be used for this, as there is no command to
reset the prompt from within a `bind -x` handler.
Contributing and Support
------------------------
Use this project's GitHub to report any bugs, request features, or submit pull
requests. If you want to discuss this project, or maybe just hang out with
the developer, feel free to join me at irc://irc.janouch.name, channel #dev.
Bitcoin donations: 12r5uEWEgcHC46xd64tt3hHt9EUvYYDHe9
License
-------
'sdn' is written by Přemysl Janouch <p.janouch@gmail.com>.
You may use the software under the terms of the ISC license, the text of which
is included within the package, or, at your option, you may relicense the work
under the MIT or the Modified BSD License, as listed at the following site:
http://www.gnu.org/licenses/license-list.html

446
sdn.cpp Normal file
View File

@ -0,0 +1,446 @@
//
// sdn: simple directory navigator
//
// Copyright (c) 2017, Přemysl Janouch <p.janouch@gmail.com>
//
// 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.
//
#include <string>
#include <vector>
#include <locale>
#include <iostream>
#include <algorithm>
#include <cwchar>
#include <climits>
#include <ncurses.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/inotify.h>
#include <fcntl.h>
// Unicode is complex enough already and we might make assumptions
#ifndef __STDC_ISO_10646__
#error Unicode required for wchar_t
#endif
using namespace std;
// For some reason handling of encoding in C and C++ is extremely annoying
// and C++17 ironically obsoletes C++11 additions that made it less painful
static wstring
to_wide (const string &multi) {
wstring wide; wchar_t w; mbstate_t mb {};
size_t n = 0, len = multi.length () + 1;
while (auto res = mbrtowc (&w, multi.c_str () + n, len - n, &mb)) {
if (res == size_t (-1) || res == size_t (-2))
return L"/invalid encoding/";
n += res;
wide += w;
}
return wide;
}
static string
to_mb (const wstring &wide) {
string mb; char buf[MB_LEN_MAX + 1]; mbstate_t mbs {};
for (size_t n = 0; n <= wide.length (); n++) {
auto res = wcrtomb (buf, wide.c_str ()[n], &mbs);
if (res == size_t (-1))
throw invalid_argument ("invalid encoding");
mb.append (buf, res);
}
// There's one extra NUL character added by wcrtomb()
mb.erase (mb.length () - 1);
return mb;
}
static int
print (const wstring &wide, int limit) {
int total_width = 0;
for (wchar_t w : wide) {
// TODO: controls as ^X, show in inverse
if (!isprint (w))
w = L'?';
int width = wcwidth (w);
if (total_width + width > limit)
break;
cchar_t c = {};
c.chars[0] = w;
add_wch (&c);
total_width += width;
}
return total_width;
}
static int
prefix (const wstring &in, const wstring &of) {
int score = 0;
for (size_t i = 0; i < of.size () && in.size () >= i && in[i] == of[i]; i++)
score++;
return score;
}
static string
shell_escape (const string &v) {
string result;
for (auto c : v)
if (c == '\'')
result += "'\\''";
else
result += c;
return "'" + result + "'";
}
// --- Application -------------------------------------------------------------
#define CTRL 31 &
struct entry {
string filename;
struct stat info;
bool operator< (const entry &other) {
auto a = S_ISDIR (info.st_mode);
auto b = S_ISDIR (other.info.st_mode);
return (a && !b) || (a == b && filename < other.filename);
}
};
// Between std and ncurses, make at least the globals stand out
static struct {
string cwd;
vector<entry> entries;
int offset, cursor;
string chosen;
bool chosen_full;
int inotify_fd, inotify_wd = -1;
bool out_of_date;
wchar_t editor;
wstring editor_line;
} g;
static inline int visible_lines () { return LINES - 2; }
static void
update () {
erase ();
attrset (A_BOLD);
mvprintw (0, 0, "%s", g.cwd.c_str ());
if (g.out_of_date)
addstr (" [+]");
for (int i = 0; i < visible_lines (); i++) {
int index = g.offset + i;
if (index >= int (g.entries.size ()))
break;
attrset (0);
if (index == g.cursor)
attron (A_REVERSE);
move (2 + i, 0);
auto &entry = g.entries[index];
// TODO display more information from "info"
char modes[] = "- ";
const auto &stat = entry.info;
if (S_ISDIR (stat.st_mode)) modes[0] = 'd';
if (S_ISBLK (stat.st_mode)) modes[0] = 'b';
if (S_ISCHR (stat.st_mode)) modes[0] = 'c';
if (S_ISLNK (stat.st_mode)) modes[0] = 'l';
if (S_ISFIFO (stat.st_mode)) modes[0] = 'p';
if (S_ISSOCK (stat.st_mode)) modes[0] = 's';
addstr (modes);
// TODO show symbolic link target
auto width = COLS - 2;
hline (' ', width - print (to_wide (entry.filename), width));
}
attrset (0);
if (g.editor) {
move (1, 0);
wchar_t prefix[] = { g.editor, L' ', L'\0' };
addwstr (prefix);
move (1, print (g.editor_line, COLS - 3) + 2);
curs_set (1);
} else
curs_set (0);
refresh ();
}
static void
reload () {
char buf[4096];
g.cwd = getcwd (buf, sizeof buf);
auto dir = opendir (".");
g.entries.clear ();
while (auto f = readdir (dir)) {
// Two dots are for navigation but this ain't as useful
if (f->d_name == string ("."))
continue;
struct stat sb = {};
lstat (f->d_name, &sb);
g.entries.push_back ({ f->d_name, sb });
}
closedir (dir);
sort (begin (g.entries), end (g.entries));
g.out_of_date = false;
g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
g.offset = min (g.offset, int (g.entries.size ()) - 1);
update ();
if (g.inotify_wd != -1)
inotify_rm_watch (g.inotify_fd, g.inotify_wd);
g.inotify_wd = inotify_add_watch (g.inotify_fd, buf,
IN_ALL_EVENTS | IN_ONLYDIR);
}
static void
search (const wstring &needle) {
int best = g.cursor, best_n = 0;
for (int i = 0; i < int (g.entries.size ()); i++) {
auto o = (i + g.cursor) % g.entries.size ();
int n = prefix (to_wide (g.entries[o].filename), needle);
if (n > best_n) {
best = o;
best_n = n;
}
}
g.cursor = best;
}
static void
handle_editor (wint_t c, bool is_char) {
if (c == 27 || c == (CTRL L'g')) {
g.editor_line.clear ();
g.editor = 0;
} else if (c == L'\r' || (!is_char && c == KEY_ENTER)) {
if (g.editor == L'e') {
auto mb = to_mb (g.editor_line);
rename (g.entries[g.cursor].filename.c_str (), mb.c_str ());
reload ();
}
g.editor_line.clear ();
g.editor = 0;
} else if (is_char) {
g.editor_line += c;
if (g.editor == L'/'
|| g.editor == L's')
search (g.editor_line);
} else if (c == KEY_BACKSPACE) {
if (!g.editor_line.empty ())
g.editor_line.erase (g.editor_line.length () - 1);
} else
beep ();
}
static bool
handle (wint_t c, bool is_char) {
// If an editor is active, let it handle the key instead and eat it
if (g.editor) {
handle_editor (c, is_char);
c = WEOF;
}
// Translate the Alt key into a bit outside the range of Unicode
enum { ALT = 1 << 24 };
if (c == 27) {
if (get_wch (&c) == ERR) {
beep ();
return true;
}
c |= ALT;
}
const auto &current = g.entries[g.cursor];
switch (c) {
case ALT | L'\r':
case ALT | KEY_ENTER:
g.chosen_full = true;
g.chosen = current.filename;
return false;
case L'\r':
case KEY_ENTER:
{
bool is_dir = S_ISDIR (current.info.st_mode) != 0;
// Dive into directories and accessible symlinks to them
if (S_ISLNK (current.info.st_mode)) {
char buf[PATH_MAX];
struct stat sb = {};
auto len = readlink (current.filename.c_str (), buf, sizeof buf);
is_dir = len > 0 && size_t (len) < sizeof buf
&& !stat (current.filename.c_str (), &sb)
&& S_ISDIR (sb.st_mode) != 0;
}
if (!is_dir) {
g.chosen = current.filename;
return false;
}
if (!chdir (current.filename.c_str ())) {
g.cursor = 0;
reload ();
}
break;
}
// M-o ought to be the same shortcut the navigator is launched with
case ALT | L'o':
case L'q':
return false;
case L'k': case CTRL L'p': case KEY_UP:
g.cursor--;
break;
case L'j': case CTRL L'n': case KEY_DOWN:
g.cursor++;
break;
case L'g': case ALT | L'<': case KEY_HOME:
g.cursor = 0;
break;
case L'G': case ALT | L'>': case KEY_END:
g.cursor = int (g.entries.size ()) - 1;
break;
case KEY_PPAGE: g.cursor -= LINES; break;
case KEY_NPAGE: g.cursor += LINES; break;
case CTRL L'e': g.offset++; break;
case CTRL L'y': g.offset--; break;
case ALT | L'e':
g.editor_line = to_wide (current.filename);
// Fall-through
case L'e':
g.editor = c & ~ALT;
break;
case L'/':
case L's':
g.editor = c;
break;
case CTRL L'L':
clear ();
break;
case L'r':
reload ();
break;
case KEY_RESIZE:
case WEOF:
break;
default:
beep ();
}
g.cursor = max (g.cursor, 0);
g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
// Make sure cursor is visible
g.offset = max (g.offset, 0);
g.offset = min (g.offset, int (g.entries.size ()) - 1);
if (g.offset > g.cursor)
g.offset = g.cursor;
if (g.cursor - g.offset >= visible_lines ())
g.offset = g.cursor - visible_lines () + 1;
update ();
return true;
}
static void
inotify_check () {
// Only provide simple indication that contents might have changed
char buf[4096]; ssize_t len;
bool changed = false;
while ((len = read (g.inotify_fd, buf, sizeof buf)) > 0) {
const inotify_event *e;
for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) {
e = (const inotify_event *) buf;
if (e->wd == g.inotify_wd)
changed = true;
}
}
if (changed)
update ();
}
int
main (int argc, char *argv[]) {
(void) argc;
(void) argv;
// That bitch zle closes stdin before exec without redirection
(void) close (STDIN_FILENO);
if (open ("/dev/tty", O_RDWR)) {
cerr << "cannot open tty" << endl;
return 1;
}
// Save the original stdout and force ncurses to use the terminal directly
auto output_fd = dup (STDOUT_FILENO);
dup2 (STDIN_FILENO, STDOUT_FILENO);
if ((g.inotify_fd = inotify_init1 (IN_NONBLOCK)) < 0) {
cerr << "cannot initialize inotify" << endl;
return 1;
}
locale::global (locale (""));
if (!initscr () || cbreak () == ERR || noecho () == ERR || nonl () == ERR
|| halfdelay (1) == ERR || keypad (stdscr, TRUE) == ERR) {
cerr << "cannot initialize screen" << endl;
return 1;
}
reload ();
auto start_dir = g.cwd;
wint_t c;
while (1) {
inotify_check ();
int res = get_wch (&c);
if (res != ERR && !handle (c, res == OK))
break;
}
endwin ();
// Presumably it is going to end up as an argument, so quote it
if (!g.chosen.empty ())
g.chosen = shell_escape (g.chosen);
// We can't portably create a standard stream from an FD, so modify the FD
dup2 (output_fd, STDOUT_FILENO);
if (g.chosen_full) {
auto full_path = g.cwd + "/" + g.chosen;
cout << "local insert=" << shell_escape (full_path) << endl;
return 0;
}
if (g.cwd != start_dir)
cout << "local cd=" << shell_escape (g.cwd) << endl;
if (!g.chosen.empty ())
cout << "local insert=" << shell_escape (g.chosen) << endl;
return 0;
}