Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dosbox-staging/dosbox-staging.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFeralChild64 <unknown>2022-10-12 01:19:18 +0300
committerkcgen <1557255+kcgen@users.noreply.github.com>2022-10-26 21:48:19 +0300
commitc8a04435cc4b8faed55833f64684d8a67f9d5570 (patch)
treede422391c6adce058d0066ea68d91ed07a0a81b3
parentf6b707ab9e654c871dab3f10ec28d66aa7b81283 (diff)
Implement MORE.COM command
-rw-r--r--src/dos/dos_programs.cpp2
-rw-r--r--src/dos/meson.build1
-rw-r--r--src/dos/program_more.cpp504
-rw-r--r--src/dos/program_more.h82
-rw-r--r--vs/dosbox.vcxproj1
-rw-r--r--vs/dosbox.vcxproj.filters3
6 files changed, 593 insertions, 0 deletions
diff --git a/src/dos/dos_programs.cpp b/src/dos/dos_programs.cpp
index 3e587a2e3..617fb9f46 100644
--- a/src/dos/dos_programs.cpp
+++ b/src/dos/dos_programs.cpp
@@ -32,6 +32,7 @@
#include "program_loadrom.h"
#include "program_ls.h"
#include "program_mem.h"
+#include "program_more.h"
#include "program_mount.h"
#include "program_mousectl.h"
#include "program_placeholder.h"
@@ -75,6 +76,7 @@ void Add_VFiles(const bool add_autoexec)
PROGRAMS_MakeFile("LOADROM.COM", ProgramCreate<LOADROM>);
PROGRAMS_MakeFile("LS.COM", ProgramCreate<LS>);
PROGRAMS_MakeFile("MEM.COM", ProgramCreate<MEM>);
+ PROGRAMS_MakeFile("MORE.COM", ProgramCreate<MORE>);
PROGRAMS_MakeFile("MOUNT.COM", ProgramCreate<MOUNT>);
PROGRAMS_MakeFile("MOUSECTL.COM", ProgramCreate<MOUSECTL>);
PROGRAMS_MakeFile("RESCAN.COM", ProgramCreate<RESCAN>);
diff --git a/src/dos/meson.build b/src/dos/meson.build
index c7521b460..da69c78cc 100644
--- a/src/dos/meson.build
+++ b/src/dos/meson.build
@@ -34,6 +34,7 @@ libdos_sources = files(
'program_loadrom.cpp',
'program_ls.cpp',
'program_mem.cpp',
+ 'program_more.cpp',
'program_mount.cpp',
'program_mount_common.cpp',
'program_mousectl.cpp',
diff --git a/src/dos/program_more.cpp b/src/dos/program_more.cpp
new file mode 100644
index 000000000..ba1849702
--- /dev/null
+++ b/src/dos/program_more.cpp
@@ -0,0 +1,504 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Copyright (C) 2022-2022 The DOSBox Staging Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "program_more.h"
+
+#include "../ints/int10.h"
+#include "callback.h"
+#include "checks.h"
+#include "dos_inc.h"
+#include "string_utils.h"
+
+#include <algorithm>
+#include <array>
+#include <cctype>
+
+CHECK_NARROWING();
+
+// ASCII control characters
+constexpr char code_ctrl_c = 0x03; // end of text
+constexpr char code_lf = 0x0a; // line feed
+constexpr char code_cr = 0x0d; // carriage return
+constexpr char code_esc = 0x1b; // escape
+
+void MORE::Run()
+{
+ // Handle command line
+ if (HelpRequested()) {
+ WriteOut(MSG_Get("SHELL_CMD_MORE_HELP_LONG"));
+ return;
+ }
+ if (!ParseCommandLine() || shutdown_requested)
+ return;
+
+ // Retrieve screen size, prepare limits
+ constexpr uint16_t min_lines = 10;
+ constexpr uint16_t min_columns = 40;
+ max_lines = std::max(min_lines, INT10_GetTextRows());
+ max_columns = std::max(min_columns, INT10_GetTextColumns());
+ // The prompt at the bottom will cause scrolling,
+ // so reduce the maximum number of lines accordingly
+ max_lines = static_cast<uint16_t>(max_lines - 1);
+
+ line_counter = 0;
+
+ // Show STDIN or input file(s) content
+ if (input_files.empty()) {
+ DisplayInputStream();
+ } else {
+ DisplayInputFiles();
+
+ // End message and command prompt is going to appear; ensure the
+ // scrolling won't make top lines disappear before user reads them
+ const int free_rows_threshold = 2;
+ if (max_lines - line_counter < free_rows_threshold)
+ PromptUser();
+
+ WriteOut(MSG_Get("SHELL_CMD_MORE_END"));
+ WriteOut("\n");
+ }
+
+ WriteOut("\n");
+}
+
+bool MORE::ParseCommandLine()
+{
+ // Put all the parameters into vector
+ std::vector<std::string> params;
+ cmd->FillVector(params);
+
+ // Check if specified tabulation size
+ if (!params.empty()) {
+ const auto &param = params[0];
+ if ((starts_with("/t", param) || starts_with("/T", param)) &&
+ (param.length() == 3) && (param.back() >= '1') &&
+ (param.back() <= '9')) {
+ // FreeDOS extension - custom TAB size
+ tab_size = static_cast<uint8_t>(param.back() - '0');
+ params.erase(params.begin());
+ }
+ }
+
+ // Make sure no other switches are supplied
+ for (const auto &param : params)
+ if (starts_with("/", param)) {
+ WriteOut(MSG_Get("SHELL_ILLEGAL_SWITCH"), param.c_str());
+ return false;
+ }
+
+ // Create list of input files
+ return FindInputFiles(params);
+}
+
+bool MORE::FindInputFiles(const std::vector<std::string> &params)
+{
+ input_files.clear();
+ if (params.empty())
+ return true;
+
+ constexpr auto search_attr = UINT16_MAX & ~DOS_ATTR_DIRECTORY &
+ ~DOS_ATTR_VOLUME;
+
+ RealPt save_dta = dos.dta();
+ dos.dta(dos.tables.tempdta);
+
+ for (const auto &param : params) {
+ // Retrieve path to current file/pattern
+ char path[DOS_PATHLENGTH];
+ if (!DOS_Canonicalize(param.c_str(), path))
+ continue;
+ char *const end = strrchr(path, '\\') + 1;
+ assert(end);
+ *end = 0;
+
+ // Search for the first file from pattern
+ if (!DOS_FindFirst(param.c_str(),
+ static_cast<uint16_t>(search_attr))) {
+ LOG_WARNING("DOS: MORE.COM - no match for pattern '%s'",
+ param.c_str());
+ continue;
+ }
+
+ while (!shutdown_requested) {
+ CALLBACK_Idle();
+
+ char name[DOS_NAMELENGTH_ASCII];
+ uint32_t size = 0;
+ uint16_t time = 0;
+ uint16_t date = 0;
+ uint8_t attr = 0;
+
+ DOS_DTA dta(dos.dta());
+ dta.GetResult(name, size, date, time, attr);
+ assert(name);
+
+ input_files.emplace_back();
+ auto &entry = input_files.back();
+
+ if (attr & DOS_ATTR_DEVICE) {
+ entry.is_device = true;
+ entry.name = std::string(name);
+ } else {
+ entry.is_device = false;
+ entry.name = std::string(path) + std::string(name);
+ }
+
+ if (!DOS_FindNext()) {
+ break;
+ }
+ }
+ }
+
+ dos.dta(save_dta);
+
+ if (!shutdown_requested && input_files.empty()) {
+ WriteOut(MSG_Get("SHELL_CMD_MORE_NO_FILE"));
+ WriteOut("\n");
+ return false;
+ }
+
+ return true;
+}
+
+std::string MORE::GetShortName(const std::string &file_name, const char *msg_id)
+{
+ assert(msg_id);
+
+ // The shortest name we should be able to display is:
+ // - 3 dots
+ // - 1 path separator
+ // - 8 characters of name
+ // - 1 dot
+ // - 3 characters of extension
+ // This gives 16 characters.
+ // We need to keep the last column free (reduces max length by 1).
+ // Format string contains '%s' (increases max length by 2).
+ constexpr size_t min = 16;
+ const auto max_len = std::max(min, max_columns - std::strlen(MSG_Get(msg_id)) + 1);
+
+ // Nothing to do if file name maches the constraint
+ if (file_name.length() <= max_len)
+ return file_name;
+
+ // We need to shorten the name - try to strip part of the path
+ auto shortened = file_name;
+ while (shortened.length() > max_len &&
+ std::count(shortened.begin(), shortened.end(), '\\') > 1) {
+ // Strip one level of path at a time
+ const auto pos = shortened.find('\\', shortened.find('\\') + 1);
+ shortened = std::string("...") + shortened.substr(pos);
+ }
+
+ // If still too long, just cut away the beginning
+ const auto len = shortened.length();
+ if (len > max_len)
+ shortened = std::string("...") + shortened.substr(len - max_len + 3);
+
+ return shortened;
+}
+
+void MORE::DisplayInputFiles()
+{
+ WriteOut("\n");
+
+ bool first = true;
+ for (const auto &input_file : input_files) {
+ if (!first && Decision::Terminate == PromptUser())
+ break;
+ first = false;
+
+ if (!DOS_OpenFile(input_file.name.c_str(), 0, &input_handle)) {
+ LOG_WARNING("DOS: MORE.COM - could not open '%s'",
+ input_file.name.c_str());
+ const auto short_name = GetShortName(input_file.name,
+ "SHELL_CMD_MORE_OPEN_ERROR");
+ WriteOut(MSG_Get("SHELL_CMD_MORE_OPEN_ERROR"), short_name.c_str());
+ WriteOut("\n");
+ ++line_counter;
+ continue;
+ }
+
+ if (input_file.is_device) {
+ const auto short_name = GetShortName(input_file.name,
+ "SHELL_CMD_MORE_NEW_DEVICE");
+ WriteOut(MSG_Get("SHELL_CMD_MORE_NEW_DEVICE"), short_name.c_str());
+ } else {
+ const auto short_name = GetShortName(input_file.name,
+ "SHELL_CMD_MORE_NEW_FILE");
+ WriteOut(MSG_Get("SHELL_CMD_MORE_NEW_FILE"), short_name.c_str());
+ }
+ WriteOut("\n");
+ ++line_counter;
+
+ // If input from a device, CTRL+C shall quit
+ ctrl_c_enable = input_file.is_device;
+
+ const auto decision = DisplaySingleStream();
+ DOS_CloseFile(input_handle);
+ if (decision == Decision::Terminate) {
+ break;
+ }
+ }
+}
+
+void MORE::DisplayInputStream()
+{
+ // We need to be able to read STDIN for key presses, but it is most
+ // likely redirected - so clone the handle, and reconstruct real STDIN
+ // from STDERR (idea from FreeDOS implementation,
+ // https://github.com/FDOS/more/blob/master/src/more.c)
+ if (!DOS_DuplicateEntry(STDIN, &input_handle) ||
+ !DOS_ForceDuplicateEntry(STDERR, STDIN)) {
+ LOG_ERR("DOS: Unable to prepare handles in MORE.COM");
+ return;
+ }
+
+ WriteOut("\n");
+
+ // Since this CAN be STDIN input (there is no way to check),
+ // CTRL+C shall quit
+ ctrl_c_enable = true;
+ DisplaySingleStream();
+}
+
+MORE::Decision MORE::DisplaySingleStream()
+{
+ auto previous_column = GetCurrentColumn();
+
+ tabs_remaining = 0;
+ skip_next_cr = false;
+ skip_next_lf = false;
+
+ auto decision = Decision::NextFile;
+ while (true) {
+ if (shutdown_requested) {
+ decision = Decision::Terminate;
+ break;
+ }
+
+ // Read character
+ char code = 0;
+ if (!GetCharacter(code)) {
+ decision = Decision::NextFile; // end of file
+ break;
+ }
+
+ // A trick to make it more resistant to ANSI cursor movements
+ const auto current_row = GetCurrentRow();
+ if (line_counter > current_row)
+ line_counter = current_row;
+
+ // Handle new line characters
+ bool new_line = false;
+ if (code == code_cr) {
+ skip_next_lf = true;
+ new_line = true;
+ } else if (code == code_lf) {
+ skip_next_cr = true;
+ new_line = true;
+ } else {
+ skip_next_cr = false;
+ skip_next_lf = false;
+ }
+
+ // Duplicate character on the output
+ if (new_line)
+ code = '\n';
+ WriteOut("%c", code);
+
+ // Detect 'new line' due to character passing the last column
+ const auto current_column = GetCurrentColumn();
+ if (!current_column && previous_column) {
+ new_line = true;
+ }
+ previous_column = current_column;
+
+ // Update new line counter, decide if pause needed
+ if (new_line && current_row) {
+ ++line_counter;
+ }
+ if (line_counter < max_lines) {
+ continue;
+ }
+
+ // New line occured just enough times for a pause
+ decision = PromptUser();
+ if (decision == Decision::Terminate || decision == Decision::NextFile) {
+ break;
+ }
+ }
+
+ if (GetCurrentColumn()) {
+ ++line_counter;
+ WriteOut("\n");
+ }
+
+ return decision;
+}
+
+MORE::Decision MORE::PromptUser()
+{
+ line_counter = 0;
+ const bool multiple_files = input_files.size() > 1;
+
+ if (GetCurrentColumn())
+ WriteOut("\n");
+
+ if (multiple_files)
+ WriteOut(MSG_Get("SHELL_CMD_MORE_PROMPT_MULTI"));
+ else
+ WriteOut(MSG_Get("SHELL_CMD_MORE_PROMPT_SINGLE"));
+
+ auto decision = Decision::Terminate;
+ while (!shutdown_requested) {
+ CALLBACK_Idle();
+
+ uint16_t count = 1;
+ char choice = 0;
+ DOS_ReadFile(STDIN, reinterpret_cast<uint8_t *>(&choice), &count);
+
+ if (count == 0 || choice == code_ctrl_c || choice == code_esc ||
+ choice == 'q' || choice == 'Q') {
+ decision = Decision::Terminate;
+ break;
+ }
+
+ if (choice == code_cr || choice == ' ') {
+ decision = Decision::More;
+ break;
+ }
+
+ if (!multiple_files)
+ continue;
+
+ if (choice == 'n' || choice == 'N') {
+ decision = Decision::NextFile;
+ break;
+ }
+ }
+
+ if (decision == Decision::Terminate || decision == Decision::NextFile) {
+ WriteOut(" ");
+ WriteOut(MSG_Get("SHELL_CMD_MORE_TERMINATE"));
+ WriteOut("\n");
+ ++line_counter;
+ } else {
+ // We are going to continue - erase the prompt
+ WriteOut("\033[M"); // clear line
+ auto counter = GetCurrentColumn();
+ while (counter--)
+ WriteOut("\033[D"); // cursor one position back
+ }
+
+ return decision;
+}
+
+bool MORE::GetCharacter(char &code)
+{
+ if (!tabs_remaining) {
+ while (true) {
+ // Retrieve character from input stream
+ uint16_t count = 1;
+ DOS_ReadFile(input_handle,
+ reinterpret_cast<uint8_t *>(&code),
+ &count);
+
+ if (!count) {
+ return false; // end of stream
+ }
+
+ if (ctrl_c_enable && code == code_ctrl_c) {
+ if (input_files.empty()) {
+ WriteOut("^C");
+ }
+ return false; // quit by CTRL+C
+ }
+
+ // Skip CR/LF characters if requested
+ if (skip_next_cr && code == code_cr) {
+ skip_next_cr = false;
+ } else if (skip_next_lf && code == code_lf) {
+ skip_next_lf = false;
+ } else {
+ break;
+ }
+ }
+
+ // If TAB found, replace it with given number of spaces
+ if (code == '\t') {
+ tabs_remaining = tab_size;
+ }
+ }
+
+ if (tabs_remaining) {
+ --tabs_remaining;
+ code = ' ';
+ }
+
+ return true;
+}
+
+uint8_t MORE::GetCurrentColumn()
+{
+ const auto page = real_readb(BIOSMEM_SEG, BIOSMEM_CURRENT_PAGE);
+ return CURSOR_POS_COL(page);
+}
+
+uint8_t MORE::GetCurrentRow()
+{
+ const auto page = real_readb(BIOSMEM_SEG, BIOSMEM_CURRENT_PAGE);
+ return CURSOR_POS_ROW(page);
+}
+
+void MORE::AddMessages()
+{
+ MSG_Add("SHELL_CMD_MORE_HELP",
+ "Display command output or text file one screen at a time.\n");
+ MSG_Add("SHELL_CMD_MORE_HELP_LONG",
+ "Display command output or text file one screen at a time.\n"
+ "\n"
+ "Usage:\n"
+ " [color=cyan]COMMAND[reset] | [color=green]more[reset] [/t[color=white]n[reset]]\n"
+ " [color=green]more[reset] [/t[color=white]n[reset]] < [color=cyan]FILE[reset]\n"
+ " [color=green]more[reset] [/t[color=white]n[reset]] [color=cyan]PATTERN[reset] [[color=cyan]PATTERN[reset] ...]\n"
+ "\n"
+ "Where:\n"
+ " [color=cyan]COMMAND[reset] is the command to display the output of.\n"
+ " [color=cyan]FILE[reset] is an exact name of the file to display, optionally with a path.\n"
+ " [color=cyan]PATTERN[reset] is either a path to a single file or a path with wildcards,\n"
+ " which are the asterisk (*) and the question mark (?).\n"
+ " [color=white]n[reset] is the tab size, 1-9, default is 8.\n"
+ "\n"
+ "Notes:\n"
+ " This command is only for viewing text files, not binary files.\n"
+ "\n"
+ "Examples:\n"
+ " [color=cyan]dir /on[reset] | [color=green]more[reset] ; displays sorted directory one screen at a time\n"
+ " [color=green]more[reset] /t[color=white]4[reset] < [color=cyan]A:\\MANUAL.TXT[reset] ; shows the file's content with tab size 4\n");
+
+ MSG_Add("SHELL_CMD_MORE_NO_FILE", "No input file found.");
+ MSG_Add("SHELL_CMD_MORE_END", "[reset][color=light-yellow]--- end of input ---[reset]");
+ MSG_Add("SHELL_CMD_MORE_NEW_FILE", "[reset][color=light-yellow]--- file %s ---[reset]");
+ MSG_Add("SHELL_CMD_MORE_NEW_DEVICE", "[reset][color=light-yellow]--- device %s ---[reset]");
+ MSG_Add("SHELL_CMD_MORE_PROMPT_SINGLE", "[reset][color=light-yellow]--- press SPACE for more ---[reset]");
+ MSG_Add("SHELL_CMD_MORE_PROMPT_MULTI", "[reset][color=light-yellow]--- press SPACE for more, N for next file ---[reset]");
+ MSG_Add("SHELL_CMD_MORE_OPEN_ERROR", "[reset][color=red]--- could not open %s ---[reset]");
+ MSG_Add("SHELL_CMD_MORE_TERMINATE", "[reset][color=light-yellow](terminated)[reset]");
+}
diff --git a/src/dos/program_more.h b/src/dos/program_more.h
new file mode 100644
index 000000000..e5266a929
--- /dev/null
+++ b/src/dos/program_more.h
@@ -0,0 +1,82 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Copyright (C) 2022-2022 The DOSBox Staging Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef DOSBOX_PROGRAM_MORE_H
+#define DOSBOX_PROGRAM_MORE_H
+
+#include "programs.h"
+
+#include <vector>
+
+class MORE final : public Program {
+public:
+ MORE()
+ {
+ AddMessages();
+ help_detail = {HELP_Filter::All,
+ HELP_Category::Dosbox,
+ HELP_CmdType::Program,
+ "MORE"};
+ }
+ void Run();
+
+private:
+ enum class Decision {
+ More,
+ Terminate,
+ NextFile,
+ };
+
+ bool ParseCommandLine();
+ bool FindInputFiles(const std::vector<std::string> &params);
+
+ void DisplayInputFiles();
+ void DisplayInputStream();
+ Decision DisplaySingleStream();
+ Decision PromptUser();
+
+ std::string GetShortName(const std::string &file_name, const char *msg_id);
+ static uint8_t GetCurrentColumn();
+ static uint8_t GetCurrentRow();
+ bool GetCharacter(char &code);
+
+ void AddMessages();
+
+ struct InputFile {
+ std::string name = ""; // file name with path
+ bool is_device = false;
+ };
+
+ std::vector<InputFile> input_files = {};
+
+ uint16_t max_lines = 0;
+ uint16_t max_columns = 0;
+ uint16_t line_counter = 0;
+
+ uint8_t tab_size = 8;
+ uint8_t tabs_remaining = 0;
+ bool skip_next_cr = false;
+ bool skip_next_lf = false;
+
+ uint16_t input_handle = 0; // DOS handle of the input stream
+ bool ctrl_c_enable = false; // if CTRL+C in the input stream should quit
+};
+
+#endif
diff --git a/vs/dosbox.vcxproj b/vs/dosbox.vcxproj
index 0cd20276d..70e5b568c 100644
--- a/vs/dosbox.vcxproj
+++ b/vs/dosbox.vcxproj
@@ -553,6 +553,7 @@ IF %ERRORLEVEL% LSS 8 SET ERRORLEVEL = 0</Command>
<ClCompile Include="..\src\dos\program_loadrom.cpp" />
<ClCompile Include="..\src\dos\program_ls.cpp" />
<ClCompile Include="..\src\dos\program_mem.cpp" />
+ <ClCompile Include="..\src\dos\program_more.cpp" />
<ClCompile Include="..\src\dos\program_mount_common.cpp" />
<ClCompile Include="..\src\dos\program_mount.cpp" />
<ClCompile Include="..\src\dos\program_mousectl.cpp" />
diff --git a/vs/dosbox.vcxproj.filters b/vs/dosbox.vcxproj.filters
index 236e17de2..f999c4834 100644
--- a/vs/dosbox.vcxproj.filters
+++ b/vs/dosbox.vcxproj.filters
@@ -589,6 +589,9 @@
<ClCompile Include="..\src\dos\program_mem.cpp">
<Filter>src\dos</Filter>
</ClCompile>
+ <ClCompile Include="..\src\dos\program_more.cpp">
+ <Filter>src\dos</Filter>
+ </ClCompile>
<ClCompile Include="..\src\dos\program_mount.cpp">
<Filter>src\dos</Filter>
</ClCompile>