diff --git a/README.md b/README.md index 678f195350667a479757bdb95a62c45cc47aa614..5552cb1366f4caf1b02378e2488c1a8e4900a4bb 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ Missing features: - Help window - File deletion -- Opening a shell ### Improvements compared to the C version diff --git a/doc/ncdu.pod b/doc/ncdu.pod index c61dcdcdfa416d411e32564657fc542da4ddb0cd..47c1a3376a44ba708edf10d5ce2e5a129736c0a1 100644 --- a/doc/ncdu.pod +++ b/doc/ncdu.pod @@ -287,6 +287,12 @@ run ncdu as follows: export NCDU_SHELL=vifm ncdu +Ncdu will set the C<NCDU_LEVEL> environment variable or increment it before +spawning the shell. This variable allows you to detect when your shell is +running from within ncdu, which can be useful to avoid nesting multiple +instances of ncdu. Ncdu itself does not (currently) warn when attempting to run +nested instances. + =item q Quit diff --git a/src/browser.zig b/src/browser.zig index 91c4cc63fad15dd8bfb1f906f271f48a8328f16e..0c44bdea6c9210804a38d708099874d0f6375602 100644 --- a/src/browser.zig +++ b/src/browser.zig @@ -7,7 +7,7 @@ const c = @cImport(@cInclude("time.h")); usingnamespace @import("util.zig"); // Currently opened directory and its parents. -var dir_parents = model.Parents{}; +pub var dir_parents = model.Parents{}; // Sorted list of all items in the currently opened directory. // (first item may be null to indicate the "parent directory" item) @@ -305,6 +305,7 @@ const Row = struct { }; var state: enum { main, quit, info } = .main; +var message: ?[:0]const u8 = null; const quit = struct { fn draw() void { @@ -625,6 +626,13 @@ pub fn draw() void { .quit => quit.draw(), .info => info.draw(), } + if (message) |m| { + const box = ui.Box.create(6, 60, "Message"); + box.move(2, 2); + ui.addstr(m); + box.move(4, 34); + ui.addstr("Press any key to continue"); + } if (sel_row > 0) ui.move(sel_row, 0); } @@ -656,6 +664,11 @@ fn keyInputSelection(ch: i32, idx: *usize, len: usize, page: u32) bool { pub fn keyInput(ch: i32) void { defer current_view.save(); + if (message != null) { + message = null; + return; + } + switch (state) { .main => {}, // fallthrough .quit => return quit.keyInput(ch), @@ -666,13 +679,21 @@ pub fn keyInput(ch: i32) void { 'q' => if (main.config.confirm_quit) { state = .quit; } else ui.quit(), 'i' => info.set(dir_items.items[cursor_idx], .info), 'r' => { - if (main.config.imported) { - // TODO: Display message - } else { + if (main.config.imported) + message = "Directory imported from file, refreshing is disabled." + else { main.state = .refresh; scan.setupRefresh(dir_parents.copy()); } }, + 'b' => { + if (main.config.imported) + message = "Shell feature not available for imported directories." + else if (!main.config.can_shell) + message = "Shell feature disabled in read-only mode." + else + main.state = .shell; + }, // Sort & filter settings 'n' => sortToggle(.name, .asc), diff --git a/src/main.zig b/src/main.zig index 71afcd1c5c227a37d6b22209ecbc30ca2547673d..08f9980dd9b3eb5bab6ac273a5819a6d8eb534d2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -67,7 +67,7 @@ pub const config = struct { pub var confirm_quit: bool = false; }; -pub var state: enum { scan, browse, refresh } = .scan; +pub var state: enum { scan, browse, refresh, shell } = .scan; // Simple generic argument parser, supports getopt_long() style arguments. // T can be any type that has a 'fn next(T) ?[:0]const u8' method, e.g.: @@ -174,6 +174,62 @@ fn help() noreturn { std.process.exit(0); } + +fn spawnShell() void { + ui.deinit(); + defer ui.init(); + + var path = std.ArrayList(u8).init(allocator); + defer path.deinit(); + browser.dir_parents.fmtPath(true, &path); + + var env = std.process.getEnvMap(allocator) catch unreachable; + defer env.deinit(); + // NCDU_LEVEL can only count to 9, keeps the implementation simple. + if (env.get("NCDU_LEVEL")) |l| + env.put("NCDU_LEVEL", if (l.len == 0) "1" else switch (l[0]) { + '0'...'8' => @as([]const u8, &.{l[0]+1}), + '9' => "9", + else => "1" + }) catch unreachable + else + env.put("NCDU_LEVEL", "1") catch unreachable; + + const shell = std.os.getenvZ("NCDU_SHELL") orelse std.os.getenvZ("SHELL") orelse "/bin/sh"; + var child = std.ChildProcess.init(&.{shell}, allocator) catch unreachable; + defer child.deinit(); + child.cwd = path.items; + child.env_map = &env; + + const term = child.spawnAndWait() catch |e| blk: { + _ = std.io.getStdErr().writer().print( + "Error spawning shell: {s}\n\nPress enter to continue.\n", + .{ ui.errorString(e) } + ) catch {}; + _ = std.io.getStdIn().reader().skipUntilDelimiterOrEof('\n') catch unreachable; + break :blk std.ChildProcess.Term{ .Exited = 0 }; + }; + if (term != .Exited) { + const n = switch (term) { + .Exited => "status", + .Signal => "signal", + .Stopped => "stopped", + .Unknown => "unknown", + }; + const v = switch (term) { + .Exited => |v| v, + .Signal => |v| v, + .Stopped => |v| v, + .Unknown => |v| v, + }; + _ = std.io.getStdErr().writer().print( + "Shell returned with {s} code {}.\n\nPress enter to continue.\n", .{ n, v } + ) catch {}; + _ = std.io.getStdIn().reader().skipUntilDelimiterOrEof('\n') catch unreachable; + } +} + + fn readExcludeFile(path: []const u8) !void { const f = try std.fs.cwd().openFile(path, .{}); defer f.close(); @@ -279,12 +335,18 @@ pub fn main() void { browser.loadDir(); while (true) { - if (state == .refresh) { - scan.scan(); - state = .browse; - browser.loadDir(); - } else - handleEvent(true, false); + switch (state) { + .refresh => { + scan.scan(); + state = .browse; + browser.loadDir(); + }, + .shell => { + spawnShell(); + state = .browse; + }, + else => handleEvent(true, false) + } } } @@ -298,6 +360,7 @@ pub fn handleEvent(block: bool, force_draw: bool) void { switch (state) { .scan, .refresh => scan.draw(), .browse => browser.draw(), + .shell => unreachable, } if (ui.inited) _ = ui.c.refresh(); event_delay_timer.reset(); @@ -315,6 +378,7 @@ pub fn handleEvent(block: bool, force_draw: bool) void { switch (state) { .scan, .refresh => scan.keyInput(ch), .browse => browser.keyInput(ch), + .shell => unreachable, } firstblock = false; } diff --git a/src/ui.zig b/src/ui.zig index 628cefe4310453e70872ba3e7b4789c415138d15..7910ee52076c31d4ca93dc6aa35e17439cb434ee 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -52,19 +52,25 @@ pub fn oom() void { // (Would be nicer if Zig just exposed errno so I could call strerror() directly) pub fn errorString(e: anyerror) [:0]const u8 { return switch (e) { + error.AccessDenied => "Access denied", error.DiskQuota => "Disk quota exceeded", + error.FileNotFound => "No such file or directory", + error.FileSystem => "I/O error", // This one is shit, Zig uses this for both EIO and ELOOP in execve(). error.FileTooBig => "File too big", + error.FileBusy => "File is busy", error.InputOutput => "I/O error", + error.InvalidExe => "Invalid executable", + error.IsDir => "Is a directory", + error.NameTooLong => "Filename too long", error.NoSpaceLeft => "No space left on device", - error.AccessDenied => "Access denied", - error.SymlinkLoop => "Symlink loop", + error.NotDir => "Not a directory", + error.OutOfMemory, error.SystemResources => "Out of memory", error.ProcessFdQuotaExceeded => "Process file descriptor limit exceeded", + error.SymlinkLoop => "Symlink loop", error.SystemFdQuotaExceeded => "System file descriptor limit exceeded", - error.NameTooLong => "Filename too long", - error.FileNotFound => "No such file or directory", - error.IsDir => "Is a directory", - error.NotDir => "Not a directory", else => "Unknown error", // rather useless :( + // ^ TODO: remove that one and accept only a restricted error set for + // compile-time exhaustiveness checks. }; }