#include #include #include #include #include #include #include #include #include #include #include "session.h" #include "shell.h" #include "theme.h" #include "parser.h" #include "commands.h" static std::map commands; ShellCommand::ShellCommand(std::string _name, ShellFunction _function, ShellCompleter _completer, std::string _shortd, std::string _longd): function {_function}, completer {_completer}, short_description {_shortd} { /* Left trim */ _longd.erase(0, _longd.find_first_not_of("\n")); /* Right trim */ int r = _longd.size(); while(r > 0 && _longd[--r] == '\n') {} _longd.erase(r+1, _longd.size()); this->long_description = _longd; commands[_name] = this; } static Session global_session; //--- // Autocompletion //--- std::vector complete_command(char const *text) { std::vector options; for(auto const &it: commands) { if(!strncmp(it.first.c_str(), text, strlen(text))) options.push_back(it.first); } return options; } std::vector complete_vspace(char const *text, Session &session) { std::vector options; for(auto const &it: session.spaces) { if(!strncmp(it.first.c_str(), text, strlen(text))) options.push_back(it.first); } return options; } std::vector complete_region(char const *text) { std::vector options; for(auto const &it: MemoryRegion::all()) { if(!strncmp(it->name.c_str(), text, strlen(text))) options.push_back(it->name); } return options; } std::vector complete_symbol(char const *text, VirtualSpace *space) { std::vector options; for(auto const &it: space->symbols.symbols) { if(!strncmp(it.name.c_str(), text, strlen(text))) options.push_back(it.name); } return options; } Parser::CompletionRequest parse_until_autocomplete(Session &session, char const *line_buffer, int point) { /* Parse partial input and try to get a category of suggestions */ std::string input(line_buffer, point); lex_repl(input); Parser p(true); p.start(); /* Read commands until the parser runs out of input and throws a CompletionRequest */ try { do { p.skip_separators(); std::string cmd = p.symbol("command"); if(commands.count(cmd) && commands[cmd]->completer) commands[cmd]->completer(session, p); } while(!lex_idle()); } catch(Parser::CompletionRequest r) { return r; } catch(Parser::SyntaxError &e) { /* Ignore syntax errors, just don't autocomplete */ } return Parser::CompletionRequest("", ""); } char *autocomplete(char const *text, int state) { static std::vector options; static size_t i = 0; if(state == 0) { Parser::CompletionRequest const &r = parse_until_autocomplete( global_session, rl_line_buffer, rl_point); if(r.category == "command") options = complete_command(text); else if(r.category == "vspace_name") options = complete_vspace(text, global_session); else if(r.category == "memory_region") options = complete_region(text); else if(r.category == "symbol" && r.space != nullptr) options = complete_symbol(text, r.space); else options.clear(); i = 0; } return (i < options.size()) ? strdup(options[i++].c_str()) : NULL; } //--- // Shell routine //--- static std::vector parse_dot(Session &, Parser &parser) { std::vector files; while(!parser.at_end()) files.push_back(parser.str()); return files; } void _dot(Session &s, std::vector const &files, bool absolute) { std::vector paths; for(auto const &file: files) { paths.push_back(absolute ? file : s.file(file).string()); } lex_include(paths); } static std::string read_interactive(Session const &s, bool &leave) { std::string prompt = "(empty)> "; if(s.current_space) { std::string name = "(none)"; for(auto &it: s.spaces) { if(&it.second == s.current_space) name = it.first; } prompt = fmt::format("{} @ 0x{:08x}> ", name, s.current_space->cursor); } /* We need to insert RL_PROMPT_{START,END}_IGNORE into the color formatting, so we trick a little bit by using a space */ std::string color = fmt::format(theme(9), " "); int space_pos = color.find(' '); std::string SC = color.substr(0, space_pos); std::string EC = color.substr(space_pos+1); std::string SI(1, RL_PROMPT_START_IGNORE); std::string EI(1, RL_PROMPT_END_IGNORE); prompt = SI+SC+EI + prompt + SI+EC+EI; /* Get a command to execute */ char *cmd_ptr = readline(prompt.c_str()); if(!cmd_ptr) { leave = true; return ""; } std::string cmd = cmd_ptr; if(strlen(cmd_ptr) > 0) add_history(cmd_ptr); free(cmd_ptr); return cmd; } int main(int argc, char **argv) { Session &s = global_session; theme_builtin("tomorrow-night"); rl_completion_entry_function = autocomplete; rl_basic_word_break_characters = " \t\n\"\\'`@$><=;|&{(" /* Readline's default */ "+-*%)"; /* Word breaks with special characters in fxos */ /* Load path into the session */ char const *fxos_path_env = std::getenv("FXOS_PATH"); if(fxos_path_env) { std::string fxos_path = fxos_path_env; size_t offset=0, end; while(true) { offset = fxos_path.find_first_not_of(":", offset); if(offset >= fxos_path.size()) break; end = fxos_path.find_first_of(":", offset); s.path.push_back(fxos_path.substr(offset, end-offset)); offset = end; } } else { fmt::print("warning: no FXOS_PATH in environment, using WD\n"); s.path.push_back(fs::current_path()); } /* Clear readline input when receiving SIGINT */ static sigjmp_buf sigint_buf; std::signal(SIGINT, [](int) { rl_free_line_state(); rl_cleanup_after_signal(); rl_line_buffer[rl_point = rl_end = rl_mark = 0] = 0; printf("\n"); siglongjmp(sigint_buf, 1); }); rl_set_signals(); /* Load command history */ char histfile[128] = ""; if(std::getenv("HOME")) snprintf(histfile, 128, "%s/.fxos_history", std::getenv("HOME")); if(histfile[0]) read_history(histfile); /* Load fxosrc files from all library folders */ std::vector fxosrc_files; for(auto const &path: s.path) { if(fs::exists(path / "fxosrc")) fxosrc_files.push_back(path / "fxosrc"); } if(fxosrc_files.size() > 0) _dot(s, fxosrc_files, true); /* Shell main loop */ while(true) { /* Get a command if there isn't a file being executed */ if(lex_idle()) { while(sigsetjmp(sigint_buf, 1) != 0); bool leave = false; std::string cmdline = read_interactive(s, leave); if(leave) break; lex_repl(cmdline); } Parser parser(false); try { /* Read the next command from the lexer */ parser.start(); parser.skip_separators(); /* Read the command name */ if(parser.lookahead().type == T::END) continue; if(parser.lookahead().type == '?') { for(auto const &it: commands) { fmt::print(" {:5s} {}\n", it.first, it.second->short_description); } continue; } std::string cmd = parser.symbol("command"); if(cmd == "q" && parser.lookahead().type != '?') break; if(cmd == "p") { parser.dump_command(); } else if(commands.count(cmd)) { if(parser.lookahead().type == '?') { fmt::print("{}: {}\n\n{}\n", cmd, commands[cmd]->short_description, commands[cmd]->long_description); continue; } if(commands[cmd]->function) commands[cmd]->function(s, parser); } else { fmt::print("\e[31;1merror:\e[0m unknown command '{}'\n", cmd); } } catch(std::exception &e) { fmt::print("\e[31;1merror:\e[0m {}\n", e.what()); } /* Exhaust command input (if not all used by the command) */ parser.exhaust_until_separator(); } /* Save command history */ if(histfile[0]) write_history(histfile); return 0; } /* Register a no-op quit command for auto-completion */ static ShellCommand _q_cmd("q", NULL, NULL, "Quit", "Quits fxos."); /* Register the include command */ static ShellCommand _dot_cmd(".", [](Session &s, Parser &p) { _dot(s, parse_dot(s, p), false); }, [](Session &s, Parser &p) { parse_dot(s, p); }, "Include scripts", R"( . "" ""... Reads file paths from its string arguments, and executes each of them as a sequence of commands in the order of the command line. )");