Initial commit
This commit is contained in:
		
							
								
								
									
										46
									
								
								CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								CMakeLists.txt
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										13
									
								
								LICENSE
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										12
									
								
								Makefile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										75
									
								
								README.adoc
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										446
									
								
								sdn.cpp
									
									
									
									
									
										Normal 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 ¤t = 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;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user