summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt46
-rw-r--r--LICENSE13
-rw-r--r--Makefile12
-rw-r--r--README.adoc75
-rw-r--r--sdn.cpp446
5 files changed, 592 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..03f03c2
--- /dev/null
+++ b/CMakeLists.txt
@@ -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)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c949ca2
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..de28c23
--- /dev/null
+++ b/Makefile
@@ -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
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..e0c7dce
--- /dev/null
+++ b/README.adoc
@@ -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
diff --git a/sdn.cpp b/sdn.cpp
new file mode 100644
index 0000000..0a46a75
--- /dev/null
+++ b/sdn.cpp
@@ -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;
+}