diff --git a/pom.xml b/pom.xml index a95702d..80de110 100644 --- a/pom.xml +++ b/pom.xml @@ -114,5 +114,6 @@ testideas-websockets testideas-sgbd testideas-jersey + testideas-terminalEmulator diff --git a/testideas-terminalEmulator/.classpath b/testideas-terminalEmulator/.classpath new file mode 100644 index 0000000..166d544 --- /dev/null +++ b/testideas-terminalEmulator/.classpath @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testideas-terminalEmulator/.gitignore b/testideas-terminalEmulator/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/testideas-terminalEmulator/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/testideas-terminalEmulator/.project b/testideas-terminalEmulator/.project new file mode 100644 index 0000000..b616feb --- /dev/null +++ b/testideas-terminalEmulator/.project @@ -0,0 +1,23 @@ + + + testideas-terminalEmulator + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/testideas-terminalEmulator/.settings/org.eclipse.core.resources.prefs b/testideas-terminalEmulator/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..29abf99 --- /dev/null +++ b/testideas-terminalEmulator/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,6 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding//src/test/resources=UTF-8 +encoding/=UTF-8 diff --git a/testideas-terminalEmulator/.settings/org.eclipse.jdt.core.prefs b/testideas-terminalEmulator/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..2f5cc74 --- /dev/null +++ b/testideas-terminalEmulator/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/testideas-terminalEmulator/.settings/org.eclipse.m2e.core.prefs b/testideas-terminalEmulator/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/testideas-terminalEmulator/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/testideas-terminalEmulator/README.md b/testideas-terminalEmulator/README.md new file mode 100644 index 0000000..563d635 --- /dev/null +++ b/testideas-terminalEmulator/README.md @@ -0,0 +1,90 @@ +Test Ideas : Terminal Emulator + +A complete Unix/Linux terminal emulator implemented in Java 8 with Swing GUI. (2 versions !) + +## Version 1 + +Features: +- Virtual file system with directories and text files +- 14+ Unix-like commands (ls, cd, pwd, cat, echo, touch, mkdir, rm, rmdir, cp, mv, clear, help, exit) +- Minimalist scripting language with variables, conditions, and loops +- Swing-based graphical user interface +- Command history with up/down arrow navigation +- Basic tab completion +- Comprehensive unit and integration tests (JUnit 5) + +Usage: +1. Compile: javac -d bin src/com/terminal/emulator/**/*.java +2. Run: java -cp bin com.terminal.emulator.Main +3. Type 'help' for available commands +4. Type 'exit' to quit + +Project Structure: +com.terminal/ + - Main.java (entry point) + - TerminalState.java (global state) + - gui/ (Swing GUI classes) + - filesystem/ (Directory, TerminalFile, FileNode) + - commands/ (Command interface and implementations) + - script/ (ScriptEngine, ScriptParser, ScriptContext) + +tests/ + - filesystem/ (FileSystemTest, DirectoryTest) + - commands/ (CommandParserTest, LsCommandTest, CdCommandTest, CommandFactoryTest) + - TerminalStateTest.java + - ScriptEngineTest.java + - IntegrationTest.java + +``` +Commands Available: +ls [OPTIONS] [FILE...] - List directory contents +cd [DIR] - Change directory +pwd - Print working directory +cat [FILE...] - Concatenate and print files +echo [STRING...] - Display a line of text +touch [FILE...] - Create empty files +mkdir [DIRECTORY...] - Create directories +rm [FILE...] - Remove files +rmdir [DIRECTORY...] - Remove empty directories +cp SOURCE DEST - Copy files +mv SOURCE DEST - Move or rename files +clear - Clear the terminal screen +help [COMMAND] - Display help information +exit - Exit the terminal + +Scripting Language: +# Variables +set var=value +echo $var + +# Conditions +if [ "$var" = "value" ] + echo Equal +fi + +# File tests +if [ -f "file.txt" ] + echo File exists +fi + +# While loops +set count=0 +while [ "$count" != "5" ] + echo $count + # Note: Manual incrementation needed in this version + set count=1 +done + +# For loops +for item in a b c + echo $item +done + +Note: The scripting engine is minimal and does not support all Unix shell features. +``` + +## Version 2 + +Adding rediretiuons, pipes, better scripting... ! + +... \ No newline at end of file diff --git a/testideas-terminalEmulator/pom.xml b/testideas-terminalEmulator/pom.xml new file mode 100644 index 0000000..a3c0b33 --- /dev/null +++ b/testideas-terminalEmulator/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + gabywald + testideas + 0.0.1-SNAPSHOT + + testideas-terminalEmulator + jar + testideas-terminalEmulator + Unix/Linux terminal emulator in Java 8 with Swing + + UTF-8 + 1.8 + 1.8 + 5.8.2 + + + + + + info.picocli + picocli + 4.7.7 + compile + + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + + + + src/main/java + src/test/java + + + src/main/resources + + **/* + + + + + + src/test/resources + + **/* + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + gabywald.launchers.TerminalEmulatorLaunchers + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + + + gabywald.launchers.TerminalEmulatorLaunchers + + true + + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + \ No newline at end of file diff --git a/testideas-terminalEmulator/src/main/java/gabywald/launchers/TerminalEmulatorCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/launchers/TerminalEmulatorCommand.java new file mode 100644 index 0000000..74eb733 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/launchers/TerminalEmulatorCommand.java @@ -0,0 +1,101 @@ +package gabywald.launchers; + +import gabywald.terminal.gui.TerminalFrame; +import gabywald.terminal2.TerminalEmulator; +import gabywald.utilities.logger.Logger; +import gabywald.utilities.logger.Logger.LoggerLevel; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +/** + * + * @author Gabriel Chandesris (2026) + */ +@Command( + name = "testideas-terminalemulator", + version = "1.0", + description = "Application CLI with picocli.", + // subcommands = { EncodeCommand.class, DecodeCommand.class }, + mixinStandardHelpOptions = true) +public class TerminalEmulatorCommand implements Runnable { + + /** + * Code Command. + */ + static class Version { + enum TheEnum { version1, version2 } + + TheEnum actualValue = TheEnum.version2; + + @Option(names = {"-v1", "--version1"}, + description = "First Version of Terminal Emulator. ") + void setVersion1(boolean b) { this.actualValue = TheEnum.version1; } + + @Option(names = {"-v2", "--version2"}, + description = "Second Version of Terminal Emulator. ") + void setVersion2(boolean b) { this.actualValue = TheEnum.version2; } + + boolean isVersion1() { return (this.actualValue == TheEnum.version1); } + boolean isVersion2() { return (this.actualValue == TheEnum.version2); } + } + @ArgGroup(exclusive = true, heading = "Version Options%n", multiplicity = "1") + Version codVersion = new Version(); + + /** + * Log Level. + */ + public static class LogLevel { + // public enum TheEnum { none, error, warn, info, debug, trace } + public enum TheEnum { trace, debug, info, warn, error, none } + + TheEnum actualValue = TheEnum.none; + + @Option(names = "--debug", description = "Sets log level to DEBUG.") + void setDebug(boolean b) { this.actualValue = TheEnum.debug; } + + @Option(names = "--info", description = "Sets log level to INFO.") + void setInfo(boolean b) { this.actualValue = TheEnum.info; } + + @Option(names = "--warn", description = "Sets log level to WARN.") + void setWarn(boolean b) { this.actualValue = TheEnum.warn; } + + @Option(names = "--error", description = "Sets log level to ERROR.") + void setError(boolean b) { this.actualValue = TheEnum.error; } + + @Option(names = "--trace", description = "Sets log level to NONE.") + void setTrace(boolean b) { this.actualValue = TheEnum.trace; } + + @Option(names = "--none", description = "Sets log level to NONE.") + void setNone(boolean b) { this.actualValue = TheEnum.none; } + + } + @ArgGroup(exclusive = true, heading = "Log Level Options%n", multiplicity = "0..1") + LogLevel logLevel = new LogLevel(); + + public boolean isLogEnabled(LogLevel.TheEnum checkValue) { + return checkValue.ordinal() >= logLevel.actualValue.ordinal(); + } + + public LogLevel.TheEnum setLogLevel(LogLevel.TheEnum nextValue) { + LogLevel.TheEnum prevValue = this.logLevel.actualValue; + this.logLevel.actualValue = nextValue; + return prevValue; + } + + @Override + public void run() { + if (codVersion.isVersion1()) { + Logger.printlnLog(LoggerLevel.LL_FORUSER, "Terminal Emulator Version 1. "); + TerminalFrame frame = new TerminalFrame(); + frame.setVisible(true); + } + else if (codVersion.isVersion2()) { + Logger.printlnLog(LoggerLevel.LL_FORUSER, "Terminal Emulator Version 2. "); + new TerminalEmulator(); + } + else + { Logger.printlnLog(LoggerLevel.LL_WARNING, "Terminal Emulator UNKNOWN VERSION. "); } + } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/launchers/TerminalEmulatorLaunchers.java b/testideas-terminalEmulator/src/main/java/gabywald/launchers/TerminalEmulatorLaunchers.java new file mode 100644 index 0000000..dd20308 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/launchers/TerminalEmulatorLaunchers.java @@ -0,0 +1,21 @@ +package gabywald.launchers; + +import gabywald.utilities.logger.Logger; +import gabywald.utilities.logger.Logger.LoggerLevel; +import picocli.CommandLine; + +/** + * + * @author Gabriel Chandesris (2026) + */ +public class TerminalEmulatorLaunchers { + + public static void main(String[] args) { + TerminalEmulatorCommand tec = new TerminalEmulatorCommand(); + int exitCode = new CommandLine(tec).execute(args); + // System.exit(exitCode); + if (tec.isLogEnabled(TerminalEmulatorCommand.LogLevel.TheEnum.info)) + { Logger.printlnLog(LoggerLevel.LL_FORUSER, exitCode + "" ); } + } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/MainTerminal.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/MainTerminal.java new file mode 100644 index 0000000..57f255a --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/MainTerminal.java @@ -0,0 +1,15 @@ +package gabywald.terminal; + +import gabywald.terminal.gui.TerminalFrame; + +/** + * Main entry point for the terminal emulator application. + * @author Gabriel Chandesris (2026) + * @deprecated Use {@code gabywald.launchers.TerminalEmulatorLaunchers} + */ +public class MainTerminal { + public static void main(String[] args) { + TerminalFrame frame = new TerminalFrame(); + frame.setVisible(true); + } +} \ No newline at end of file diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/TerminalState.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/TerminalState.java new file mode 100644 index 0000000..b7c8134 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/TerminalState.java @@ -0,0 +1,84 @@ +package gabywald.terminal; + +import java.util.ArrayList; +import java.util.List; + +import gabywald.terminal.filesystem.TerminalDirectory; + +/** + * Global state of the terminal including current directory, history, etc. + * @author Gabriel Chandesris (2026) + */ +public class TerminalState { + private TerminalDirectory currentDirectory; + private TerminalDirectory rootDirectory; + private List commandHistory; + private int historyIndex; + private String prompt; + + public TerminalState() { + this.rootDirectory = new TerminalDirectory("/", null); + this.currentDirectory = this.rootDirectory; + this.commandHistory = new ArrayList<>(); + this.historyIndex = -1; + this.prompt = "user@terminal:~$"; + } + + public TerminalState(TerminalDirectory root) { + this.rootDirectory = root; + this.currentDirectory = root; + this.commandHistory = new ArrayList<>(); + this.historyIndex = -1; + this.prompt = "user@terminal:~$"; + } + + public TerminalDirectory getCurrentDirectory() { return currentDirectory; } + public void setCurrentDirectory(TerminalDirectory currentDirectory) { + this.currentDirectory = currentDirectory; + updatePrompt(); + } + public TerminalDirectory getRootDirectory() { return this.rootDirectory; } + public List getCommandHistory() { return new ArrayList(commandHistory); } + + public void addToHistory(String command) { + if (command != null && !command.trim().isEmpty()) { + this.commandHistory.add(command); + this.historyIndex = this.commandHistory.size(); + } + } + + public int getHistoryIndex() { return historyIndex; } + public void setHistoryIndex(int historyIndex) { this.historyIndex = historyIndex; } + public String getPrompt() { return this.prompt; } + + private void updatePrompt() { + String path = this.currentDirectory.getPath(); + if ("/".equals(path)) { this.prompt = "user@terminal:~$"; } + else { this.prompt = "user@terminal:" + path + "$"; } + } + + public String getPreviousCommand() { + if (this.commandHistory.isEmpty()) return null; + if (this.historyIndex > 0) { + this.historyIndex--; + return this.commandHistory.get(this.historyIndex); + } else if (this.historyIndex == 0) { + return this.commandHistory.get(0); + } + return null; + } + + public String getNextCommand() { + if (this.commandHistory.isEmpty()) return null; + if (this.historyIndex < commandHistory.size() - 1) { + this.historyIndex++; + return this.commandHistory.get(this.historyIndex); + } else if (this.historyIndex == this.commandHistory.size() - 1) { + this.historyIndex = this.commandHistory.size(); + return ""; + } + return null; + } + + public void resetHistoryIndex() { this.historyIndex = this.commandHistory.size(); } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CatCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CatCommand.java new file mode 100644 index 0000000..973b5e6 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CatCommand.java @@ -0,0 +1,39 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.filesystem.TerminalNode; +import gabywald.terminal.filesystem.TerminalFile; + +/** + * cat command - Concatenate and print files + * @author Gabriel Chandesris (2026) + */ +public class CatCommand implements Command { + @Override + public String execute(TerminalState state, String[] args) { + if (args.length == 0) return "cat: missing operand"; + + StringBuilder output = new StringBuilder(); + for (String fileName : args) { + TerminalNode node = CommandHelper.resolveFile(state, fileName); + if (node == null) { + output.append("cat: ").append(fileName).append(": No such file or directory\n"); + continue; + } + if (!node.isFile()) { + output.append("cat: ").append(fileName).append(": Is a directory\n"); + continue; + } + TerminalFile file = (TerminalFile) node; + output.append(file.getContent()); + } + return output.toString(); + } + + @Override + public String getName() { return "cat"; } + @Override + public String getDescription() { return "Concatenate and print files"; } + @Override + public String getUsage() { return "cat [FILE]..."; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CdCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CdCommand.java new file mode 100644 index 0000000..139237d --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CdCommand.java @@ -0,0 +1,34 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.filesystem.TerminalDirectory; + +/** + * cd command - Change directory + * @author Gabriel Chandesris (2026) + */ +public class CdCommand implements Command { + + @Override + public String execute(TerminalState state, String[] args) { + if (args.length == 0) { + state.setCurrentDirectory(state.getRootDirectory()); + return ""; + } + + String path = args[0]; + TerminalDirectory newDir = CommandHelper.resolveDirectory(state, path); + if (newDir == null) { return "cd: no such file or directory: " + path; } + + state.setCurrentDirectory(newDir); + return ""; + } + + @Override + public String getName() { return "cd"; } + @Override + public String getDescription() { return "Change the current directory"; } + @Override + public String getUsage() { return "cd [DIR]"; } + +} \ No newline at end of file diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/ClearCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/ClearCommand.java new file mode 100644 index 0000000..0d45be7 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/ClearCommand.java @@ -0,0 +1,21 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; + +/** + * clear command - Clear the terminal screen + * @author Gabriel Chandesris (2026) + */ +public class ClearCommand implements Command { + @Override + public String execute(TerminalState state, String[] args) { + return "\033[H\033[2J"; + } + + @Override + public String getName() { return "clear"; } + @Override + public String getDescription() { return "Clear the terminal screen"; } + @Override + public String getUsage() { return "clear"; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/Command.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/Command.java new file mode 100644 index 0000000..b8f9cef --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/Command.java @@ -0,0 +1,14 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; + +/** + * Command interface for all executable commands. + * @author Gabriel Chandesris (2026) + */ +public interface Command { + String execute(TerminalState state, String[] args); + String getName(); + String getDescription(); + String getUsage(); +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CommandFactory.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CommandFactory.java new file mode 100644 index 0000000..77a7887 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CommandFactory.java @@ -0,0 +1,44 @@ +package gabywald.terminal.commands; + +import java.util.HashMap; +import java.util.Map; + +/** + * Factory for creating and registering available commands. + * @author Gabriel Chandesris (2026) + */ +public class CommandFactory { + + private static final Map commands = new HashMap(); + + static { + CommandFactory.registerCommand(new LsCommand()); + CommandFactory.registerCommand(new CdCommand()); + CommandFactory.registerCommand(new PwdCommand()); + CommandFactory.registerCommand(new CatCommand()); + CommandFactory.registerCommand(new EchoCommand()); + CommandFactory.registerCommand(new TouchCommand()); + CommandFactory.registerCommand(new MkdirCommand()); + CommandFactory.registerCommand(new RmCommand()); + CommandFactory.registerCommand(new RmdirCommand()); + CommandFactory.registerCommand(new CpCommand()); + CommandFactory.registerCommand(new MvCommand()); + CommandFactory.registerCommand(new ClearCommand()); + CommandFactory.registerCommand(new HelpCommand()); + CommandFactory.registerCommand(new ExitCommand()); + } + + public static void registerCommand(Command command) { + if (command != null) { CommandFactory.commands.put(command.getName().toLowerCase(), command); } + } + + public static Command getCommand(String name) { + if (name == null || name.isEmpty()) { return null; } + return CommandFactory.commands.get(name.toLowerCase()); + } + + public static boolean hasCommand(String name) { return CommandFactory.getCommand(name) != null; } + public static Map getAllCommands() { return new HashMap<>(CommandFactory.commands); } + public static String[] getCommandNames() { return CommandFactory.commands.keySet().toArray(new String[0]); } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CommandHelper.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CommandHelper.java new file mode 100644 index 0000000..6abe777 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CommandHelper.java @@ -0,0 +1,144 @@ +package gabywald.terminal.commands; + +import java.time.format.DateTimeFormatter; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.filesystem.TerminalNode; +import gabywald.terminal.filesystem.TerminalDirectory; + +/** + * + * @author Gabriel Chandesris (2026) + */ +public abstract class CommandHelper { + + static TerminalDirectory getParentFromAbsolutePath(TerminalDirectory root, String path) { + String[] parts = path.split("/"); + TerminalDirectory current = root; + for (int i = 1; i < parts.length - 1; i++) { + if (parts[i].isEmpty() || ".".equals(parts[i])) { continue; } + if ("..".equals(parts[i])) { + if (current.getParent() != null) { current = current.getParent(); } + } else { + TerminalNode node = current.getChild(parts[i]); + if (node == null || !node.isDirectory()) { return null; } + current = (TerminalDirectory) node; + } + } + return current; + } + + static TerminalDirectory getParentFromRelativePath(TerminalDirectory current, String path) { + String[] parts = path.split("/"); + for (int i = 0; i < parts.length - 1; i++) { + if (parts[i].isEmpty() || ".".equals(parts[i])) { continue; } + if ("..".equals(parts[i])) { + if (current.getParent() != null) current = current.getParent(); + } else { + TerminalNode node = current.getChild(parts[i]); + if (node == null || !node.isDirectory()) { return null; } + current = (TerminalDirectory) node; + } + } + return current; + } + + static TerminalNode resolveFile(TerminalState state, String fileName) { + TerminalDirectory current = state.getCurrentDirectory(); + if (fileName.startsWith("/")) { return CommandHelper.resolveAbsolutePath(state.getRootDirectory(), fileName); } + return CommandHelper.resolveRelativePath(current, fileName); + } + + static TerminalNode resolveAbsolutePath(TerminalDirectory root, String path) { + String[] parts = path.split("/"); + TerminalDirectory current = root; + for (int i = 1; i < parts.length; i++) { + if (parts[i].isEmpty() || ".".equals(parts[i])) { continue; } + if ("..".equals(parts[i])) { + if (current.getParent() != null) { current = current.getParent(); } + } else { + TerminalNode node = current.getChild(parts[i]); + if (node == null) { return null; } + if (i == parts.length - 1) { return node; } + if (!node.isDirectory()) { return null; } + current = (TerminalDirectory) node; + } + } + return current; + } + + static TerminalNode resolveRelativePath(TerminalDirectory current, String path) { + String[] parts = path.split("/"); + for (int i = 0; i < parts.length; i++) { + if (parts[i].isEmpty() || ".".equals(parts[i])) { continue; } + if ("..".equals(parts[i])) { + if (current.getParent() != null) { current = current.getParent(); } + } else { + TerminalNode node = current.getChild(parts[i]); + if (node == null) { return null; } + if (i == parts.length - 1) { return node; } + if (!node.isDirectory()) { return null; } + current = (TerminalDirectory) node; + } + } + return current; + } + + static TerminalDirectory resolveDirectory(TerminalState state, String dirName) { + TerminalDirectory current = state.getCurrentDirectory(); + if (dirName.startsWith("/")) { return CommandHelper.resolveAbsoluteDirectory(state.getRootDirectory(), dirName); } + return CommandHelper.resolveRelativeDirectory(current, dirName); + } + + static TerminalDirectory resolveAbsoluteDirectory(TerminalDirectory root, String path) { + String[] parts = path.split("/"); + TerminalDirectory current = root; + for (int i = 1; i < parts.length; i++) { + if (parts[i].isEmpty() || ".".equals(parts[i])) { continue; } + if ("..".equals(parts[i])) { + if (current.getParent() != null) { current = current.getParent(); } + } else { + TerminalNode node = current.getChild(parts[i]); + if (node == null || !node.isDirectory()) { return null; } + current = (TerminalDirectory) node; + } + } + return current; + } + + static TerminalDirectory resolveRelativeDirectory(TerminalDirectory current, String path) { + String[] parts = path.split("/"); + for (String part : parts) { + if (part.isEmpty() || ".".equals(part)) { continue; } + if ("..".equals(part)) { + if (current.getParent() != null) { current = current.getParent(); } + } else { + TerminalNode node = current.getChild(part); + if (node == null || !node.isDirectory()) { return null; } + current = (TerminalDirectory) node; + } + } + return current; + } + + static TerminalDirectory getParentDirectory(TerminalState state, String path) { + if (path.startsWith("/")) { return CommandHelper.getParentFromAbsolutePath(state.getRootDirectory(), path); } + return CommandHelper.getParentFromRelativePath(state.getCurrentDirectory(), path); + } + + static String getSimpleName(String path) { + String[] parts = path.split("/"); + return parts[parts.length - 1]; + } + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + static String formatLong(TerminalNode node) { + String type = node.isDirectory() ? "d" : "-"; + String permissions = node.isDirectory() ? "rwxr-xr-x" : "rw-r--r--"; + String size = node.isDirectory() ? "4096" : String.valueOf(node.getSize()); + String date = node.getModifiedAt().format(DATE_FORMATTER); + return String.format("%s%s 1 user group %8s %s %s\n", type, permissions, size, date, node.getName()); + } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CommandParser.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CommandParser.java new file mode 100644 index 0000000..1461447 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CommandParser.java @@ -0,0 +1,59 @@ +package gabywald.terminal.commands; + +import java.util.ArrayList; +import java.util.List; + +/** + * Command parser for extracting command name and arguments. + * Handles quoted arguments with spaces. + * @author Gabriel Chandesris (2026) + */ +public class CommandParser { + + public static String[] parse(String input) { + if (input == null || input.trim().isEmpty()) return new String[0]; + + String trimmed = input.trim(); + List tokens = new ArrayList<>(); + boolean inQuotes = false; + StringBuilder current = new StringBuilder(); + + for (int i = 0; i < trimmed.length(); i++) { + char c = trimmed.charAt(i); + if (c == '"') { + inQuotes = !inQuotes; + } else if (Character.isWhitespace(c) && !inQuotes) { + if (current.length() > 0) { + tokens.add(current.toString()); + current.setLength(0); + } + } else { current.append(c); } + } + + if (current.length() > 0) tokens.add(current.toString()); + return tokens.toArray(new String[0]); + } + + public static String getCommandName(String input) { + String[] parts = parse(input); + return parts.length > 0 ? parts[0] : ""; + } + + public static String[] getArguments(String input) { + String[] parts = parse(input); + if (parts.length > 1) { + String[] args = new String[parts.length - 1]; + System.arraycopy(parts, 1, args, 0, args.length); + return args; + } + return new String[0]; + } + + public static boolean hasArguments(String input) { return getArguments(input).length > 0; } + + public static String cleanArgument(String arg) { + if (arg == null) { return ""; } + return arg.replaceAll("^$", "").replaceAll("\"", ""); // NOTE "^\"|"$" + } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CpCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CpCommand.java new file mode 100644 index 0000000..bf0562c --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/CpCommand.java @@ -0,0 +1,63 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.filesystem.TerminalDirectory; +import gabywald.terminal.filesystem.TerminalNode; +import gabywald.terminal.filesystem.TerminalFile; + +/** + * cp command - Copy filesabriel Chandesris (2026) + * @author Gabriel Chandesris (2026) + */ +public class CpCommand implements Command { + + @Override + public String execute(TerminalState state, String[] args) { + if (args.length < 2) { + return "cp: missing destination file operand after '" + + (args.length > 0 ? args[args.length - 1] : "") + "'\nTry 'cp --help' for more information."; + } + + if (args.length == 2) { return copyFile(state, args[0], args[1]); } + + String destination = args[args.length - 1]; + TerminalDirectory destDir = CommandHelper.resolveDirectory(state, destination); + if (destDir == null || !destDir.isDirectory()) { return "cp: target '" + destination + "' is not a directory"; } + + StringBuilder output = new StringBuilder(); + for (int i = 0; i < args.length - 1; i++) { + String result = copyFile(state, args[i], destination + "/" + CommandHelper.getSimpleName(args[i])); + if (!result.isEmpty()) { output.append(result).append("\n"); } + } + return output.toString(); + } + + private String copyFile(TerminalState state, String sourcePath, String destPath) { + TerminalNode source = CommandHelper.resolveFile(state, sourcePath); + if (source == null) return "cp: cannot stat '" + sourcePath + "': No such file or directory"; + if (source.isDirectory()) return "cp: -r not specified; omitting directory '" + sourcePath + "'"; + + TerminalDirectory destParent = CommandHelper.getParentDirectory(state, destPath); + if (destParent == null) return "cp: cannot create '" + destPath + "': No such file or directory"; + + String destName = CommandHelper.getSimpleName(destPath); + if (destParent.hasChild(destName)) { + TerminalNode existing = destParent.getChild(destName); + if (existing.isDirectory()) { return "cp: cannot overwrite directory '" + destPath + "' with '" + sourcePath + "'"; } + ((TerminalFile) existing).setContent(((TerminalFile) source).getContent()); + } else { + TerminalFile sourceFile = (TerminalFile) source; + TerminalFile newFile = new TerminalFile(destName, destParent, sourceFile.getContent()); + destParent.addChild(newFile); + } + return ""; + } + + @Override + public String getName() { return "cp"; } + @Override + public String getDescription() { return "Copy files and directories"; } + @Override + public String getUsage() { return "cp [OPTION] SOURCE DEST\n cp [OPTION] SOURCE... DIRECTORY"; } + +} \ No newline at end of file diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/EchoCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/EchoCommand.java new file mode 100644 index 0000000..985d929 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/EchoCommand.java @@ -0,0 +1,27 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; + +/** + * echo command - Display a line of text + * @author Gabriel Chandesris (2026) + */ +public class EchoCommand implements Command { + @Override + public String execute(TerminalState state, String[] args) { + if (args.length == 0) return ""; + StringBuilder output = new StringBuilder(); + for (int i = 0; i < args.length; i++) { + if (i > 0) { output.append(" "); } + output.append(args[i]); + } + return output.toString(); + } + + @Override + public String getName() { return "echo"; } + @Override + public String getDescription() { return "Display a line of text"; } + @Override + public String getUsage() { return "echo [STRING]..."; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/ExitCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/ExitCommand.java new file mode 100644 index 0000000..1604c25 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/ExitCommand.java @@ -0,0 +1,23 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; + +/** + * exit command - Exit the terminal + * @author Gabriel Chandesris (2026) + */ +public class ExitCommand implements Command { + + @Override + public String execute(TerminalState state, String[] args) { + return "EXIT"; + } + + @Override + public String getName() { return "exit"; } + @Override + public String getDescription() { return "Exit the terminal"; } + @Override + public String getUsage() { return "exit"; } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/HelpCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/HelpCommand.java new file mode 100644 index 0000000..abbc0b6 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/HelpCommand.java @@ -0,0 +1,45 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; +import java.util.Map; + +/** + * help command - Display help information + */ +public class HelpCommand implements Command { + + @Override + public String execute(TerminalState state, String[] args) { + if (args.length == 0) + { return this.getGeneralHelp(); } + else { return this.getCommandHelp(args[0]); } + } + + private String getGeneralHelp() { + StringBuilder help = new StringBuilder(); + help.append("Available commands:\n\n"); + Map commands = CommandFactory.getAllCommands(); + for (Map.Entry entry : commands.entrySet()) { + Command cmd = entry.getValue(); + help.append(String.format(" %-15s %s\n", cmd.getName(), cmd.getDescription())); + } + help.append("\nType 'help ' for more information about a specific command.\n"); + return help.toString(); + } + + private String getCommandHelp(String commandName) { + Command cmd = CommandFactory.getCommand(commandName); + if (cmd == null) + { return "help: no help topics match '" + commandName + "'.\nTry 'help' for a list of available commands."; } + return String.format("Usage: %s\n\n%s\n", cmd.getUsage(), cmd.getDescription()); + } + + @Override + public String getName() { return "help"; } + @Override + public String getDescription() { return "Display help information"; } + @Override + public String getUsage() { return "help [COMMAND]"; } + +} + \ No newline at end of file diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/LsCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/LsCommand.java new file mode 100644 index 0000000..fa3c7a7 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/LsCommand.java @@ -0,0 +1,67 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.filesystem.TerminalDirectory; +import gabywald.terminal.filesystem.TerminalNode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * ls command - List directory contents + * @author Gabriel Chandesris (2026) + */ +public class LsCommand implements Command { + + @Override + public String execute(TerminalState state, String[] args) { + boolean longFormat = false; + boolean showHidden = false; + boolean reverseOrder = false; + String targetPath = "."; + + List paths = new ArrayList<>(); + for (String arg : args) { + if (arg.startsWith("-")) { + if (arg.contains("l")) { longFormat = true; } + if (arg.contains("a")) { showHidden = true; } + if (arg.contains("r")) { reverseOrder = true; } + } else if (!arg.isEmpty()) { paths.add(arg); } + } + + if (!paths.isEmpty()) { targetPath = paths.get(0); } + + TerminalDirectory targetDir = CommandHelper.resolveDirectory(state, targetPath); + if (targetDir == null) { return "ls: cannot access '" + targetPath + "': No such file or directory"; } + + List children = new ArrayList<>(targetDir.getChildren()); + if (!showHidden) { children.removeIf(node -> node.getName().startsWith(".")); } + + final boolean reverseO = reverseOrder; + Collections.sort(children, (n1, n2) -> { + int result = n1.getName().compareTo(n2.getName()); + return (reverseO ? -result : result); + }); + + StringBuilder output = new StringBuilder(); + if (longFormat) { + output.append("total ").append(children.size()).append("\n"); + for (TerminalNode node : children) output.append(CommandHelper.formatLong(node)); + } else { + for (int i = 0; i < children.size(); i++) { + if (i > 0) { output.append(" "); } + output.append(children.get(i).getName()); + } + if (!children.isEmpty()) { output.append("\n"); } + } + return output.toString(); + } + + @Override + public String getName() { return "ls"; } + @Override + public String getDescription() { return "List directory contents"; } + @Override + public String getUsage() { return "ls [OPTION]... [FILE]..."; } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/MkdirCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/MkdirCommand.java new file mode 100644 index 0000000..f72416f --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/MkdirCommand.java @@ -0,0 +1,41 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.filesystem.TerminalDirectory; + +/** + * mkdir command - Create directories + * @author Gabriel Chandesris (2026) + */ +public class MkdirCommand implements Command { + + @Override + public String execute(TerminalState state, String[] args) { + if (args.length == 0) return "mkdir: missing operand"; + + StringBuilder output = new StringBuilder(); + for (String dirName : args) { + TerminalDirectory parentDir = CommandHelper.getParentDirectory(state, dirName); + if (parentDir == null) { + output.append("mkdir: cannot create directory '").append(dirName).append("': No such file or directory\n"); + continue; + } + + String simpleName = CommandHelper.getSimpleName(dirName); + if (parentDir.hasChild(simpleName)) { + output.append("mkdir: cannot create directory '").append(dirName).append("': File exists\n"); + continue; + } + parentDir.createDirectory(simpleName); + } + return output.toString(); + } + + @Override + public String getName() { return "mkdir"; } + @Override + public String getDescription() { return "Create directories"; } + @Override + public String getUsage() { return "mkdir DIRECTORY..."; } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/MvCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/MvCommand.java new file mode 100644 index 0000000..c9daeb0 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/MvCommand.java @@ -0,0 +1,66 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.filesystem.TerminalDirectory; +import gabywald.terminal.filesystem.TerminalNode; +import gabywald.terminal.filesystem.TerminalFile; + +/** + * mv command - Move or rename files + * @author Gabriel Chandesris (2026) + */ +public class MvCommand implements Command { + @Override + public String execute(TerminalState state, String[] args) { + if (args.length < 2) { + return "mv: missing destination file operand after '" + + (args.length > 0 ? args[args.length - 1] : "") + "'\nTry 'mv --help' for more information."; + } + + if (args.length == 2) { + return moveFile(state, args[0], args[1]); + } + + String destination = args[args.length - 1]; + TerminalDirectory destDir = CommandHelper.resolveDirectory(state, destination); + if (destDir == null || !destDir.isDirectory()) { + return "mv: target '" + destination + "' is not a directory"; + } + + StringBuilder output = new StringBuilder(); + for (int i = 0; i < args.length - 1; i++) { + String result = moveFile(state, args[i], destination + "/" + CommandHelper.getSimpleName(args[i])); + if (!result.isEmpty()) { output.append(result).append("\n"); } + } + return output.toString(); + } + + private String moveFile(TerminalState state, String sourcePath, String destPath) { + TerminalNode source = CommandHelper.resolveFile(state, sourcePath); + if (source == null) { return "mv: cannot stat '" + sourcePath + "': No such file or directory"; } + + TerminalDirectory destParent = CommandHelper.getParentDirectory(state, destPath); + if (destParent == null) { return "mv: cannot move '" + sourcePath + "' to '" + destPath + "': No such file or directory"; } + + String destName = CommandHelper.getSimpleName(destPath); + if (destParent.hasChild(destName)) { + TerminalNode existing = destParent.getChild(destName); + if (existing.isDirectory()) { return "mv: cannot overwrite directory '" + destPath + "' with '" + sourcePath + "'"; } + if (source.isFile()) { ((TerminalFile) existing).setContent(((TerminalFile) source).getContent()); } + source.delete(); + } else { + source.setName(destName); + source.getParent().removeChild(source); + destParent.addChild(source); + } + return ""; + } + + @Override + public String getName() { return "mv"; } + @Override + public String getDescription() { return "Move or rename files"; } + @Override + + public String getUsage() { return "mv [OPTION] SOURCE DEST\n mv [OPTION] SOURCE... DIRECTORY"; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/PwdCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/PwdCommand.java new file mode 100644 index 0000000..9a6ac27 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/PwdCommand.java @@ -0,0 +1,21 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; + +/** + * pwd command - Print working directory + * @author Gabriel Chandesris (2026) + */ +public class PwdCommand implements Command { + @Override + public String execute(TerminalState state, String[] args) { + return state.getCurrentDirectory().getPath(); + } + + @Override + public String getName() { return "pwd"; } + @Override + public String getDescription() { return "Print the current working directory"; } + @Override + public String getUsage() { return "pwd"; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/RmCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/RmCommand.java new file mode 100644 index 0000000..b3729ce --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/RmCommand.java @@ -0,0 +1,39 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.filesystem.TerminalNode; + +/** + * rm command - Remove files + * @author Gabriel Chandesris (2026) + */ +public class RmCommand implements Command { + @Override + public String execute(TerminalState state, String[] args) { + if (args.length == 0) { return "rm: missing operand"; } + + StringBuilder output = new StringBuilder(); + for (String fileName : args) { + TerminalNode node = CommandHelper.resolveFile(state, fileName); + if (node == null) { + output.append("rm: cannot remove '").append(fileName).append("': No such file or directory\n"); + continue; + } + if (node.isDirectory()) { + output.append("rm: cannot remove '").append(fileName).append("': Is a directory\n"); + continue; + } + if (!node.delete()) { + output.append("rm: cannot remove '").append(fileName).append("': Operation not permitted\n"); + } + } + return output.toString(); + } + + @Override + public String getName() { return "rm"; } + @Override + public String getDescription() { return "Remove files"; } + @Override + public String getUsage() { return "rm FILE..."; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/RmdirCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/RmdirCommand.java new file mode 100644 index 0000000..466b320 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/RmdirCommand.java @@ -0,0 +1,45 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.filesystem.TerminalDirectory; +import gabywald.terminal.filesystem.TerminalNode; + +/** + * rmdir command - Remove empty directories + * @author Gabriel Chandesris (2026) + */ +public class RmdirCommand implements Command { + @Override + public String execute(TerminalState state, String[] args) { + if (args.length == 0) { return "rmdir: missing operand"; } + + StringBuilder output = new StringBuilder(); + for (String dirName : args) { + TerminalNode node = CommandHelper.resolveDirectory(state, dirName); + if (node == null) { + output.append("rmdir: failed to remove '").append(dirName).append("': No such file or directory\n"); + continue; + } + if (!node.isDirectory()) { + output.append("rmdir: failed to remove '").append(dirName).append("': Not a directory\n"); + continue; + } + TerminalDirectory dir = (TerminalDirectory) node; + if (!dir.isEmpty()) { + output.append("rmdir: failed to remove '").append(dirName).append("': Directory not empty\n"); + continue; + } + if (!dir.delete()) { + output.append("rmdir: failed to remove '").append(dirName).append("': Operation not permitted\n"); + } + } + return output.toString(); + } + + @Override + public String getName() { return "rmdir"; } + @Override + public String getDescription() { return "Remove empty directories"; } + @Override + public String getUsage() { return "rmdir DIRECTORY..."; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/TouchCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/TouchCommand.java new file mode 100644 index 0000000..e710c05 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/commands/TouchCommand.java @@ -0,0 +1,61 @@ +package gabywald.terminal.commands; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.filesystem.TerminalDirectory; +import gabywald.terminal.filesystem.TerminalNode; +import gabywald.terminal.filesystem.TerminalFile; + +/** + * touch command - Create empty files or update timestamp + * @author Gabriel Chandesris (2026) + */ +public class TouchCommand implements Command { + @Override + public String execute(TerminalState state, String[] args) { + if (args.length == 0) { return "touch: missing file operand"; } + + StringBuilder output = new StringBuilder(); + for (String fileName : args) { + TerminalNode existing = resolveFile(state, fileName); + if (existing != null && existing.isDirectory()) { + output.append("touch: cannot touch '").append(fileName).append("': Is a directory\n"); + continue; + } + + TerminalDirectory parentDir = getParentDirectory(state, fileName); + if (parentDir == null) { + output.append("touch: cannot touch '").append(fileName).append("': No such file or directory\n"); + continue; + } + + String simpleName = getSimpleName(fileName); + if (existing != null && existing.isFile()) + { ((TerminalFile) existing).setContent(((TerminalFile)existing).getContent()); } + else { parentDir.createFile(simpleName); } + } + return output.toString(); + } + + private TerminalNode resolveFile(TerminalState state, String fileName) { + TerminalDirectory current = state.getCurrentDirectory(); + if (fileName.startsWith("/")) { return CommandHelper.resolveAbsolutePath(state.getRootDirectory(), fileName); } + return CommandHelper.resolveRelativePath(current, fileName); + } + + private TerminalDirectory getParentDirectory(TerminalState state, String path) { + if (path.startsWith("/")) { return CommandHelper.getParentFromAbsolutePath(state.getRootDirectory(), path); } + return CommandHelper.getParentFromRelativePath(state.getCurrentDirectory(), path); + } + + private String getSimpleName(String path) { + String[] parts = path.split("/"); + return parts[parts.length - 1]; + } + + @Override + public String getName() { return "touch"; } + @Override + public String getDescription() { return "Create empty files or update timestamp"; } + @Override + public String getUsage() { return "touch FILE..."; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/FileSystem.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/FileSystem.java new file mode 100644 index 0000000..3a2bf9f --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/FileSystem.java @@ -0,0 +1,466 @@ +package gabywald.terminal.filesystem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Classe représentant un système de fichiers simplifié pour l'émulateur de terminal. + * Gère les fichiers et répertoires sous forme de structure arborescente. + * + *

+ * Fonctionnalités principales : + *

    + *
  • Création et suppression de fichiers et répertoires.
  • + *
  • Navigation entre répertoires (cd, pwd).
  • + *
  • Lecture et écriture de fichiers (cat, echo).
  • + *
  • Vérification de l'existence de fichiers/répertoires.
  • + *
  • Gestion des chemins absolus et relatifs.
  • + *
+ *

+ * + *

+ * Exemple d'utilisation : + *

+ * FileSystem fs = new FileSystem();
+ * fs.mkdir("test");          // Crée un répertoire "test"
+ * fs.touch("file.txt");      // Crée un fichier "file.txt"
+ * fs.echo("file.txt", "Hello"); // Écrit "Hello" dans "file.txt"
+ * String content = fs.cat("file.txt"); // Lit le contenu
+ * 
+ *

+ */ +public class FileSystem { + // Répertoire racine (constante) + private static final String ROOT = "/"; + + // Répertoire courant + private String currentDirectory; + + // Structure des répertoires : clé = chemin absolu, valeur = ensemble des noms de fichiers/répertoires + private Map> directories; + + // Structure des fichiers : clé = chemin absolu, valeur = contenu du fichier + private Map files; + + /** + * Constructeur : initialise le système de fichiers avec un répertoire racine. + * Le répertoire racine est créé automatiquement. + */ + public FileSystem() { + this.currentDirectory = FileSystem.ROOT; + this.directories = new HashMap<>(); + this.files = new HashMap<>(); + + // Initialiser le répertoire racine + this.directories.put(ROOT, new HashSet<>()); + } + + // ============================================ + // Méthodes de base pour le système de fichiers + // ============================================ + + /** + * Retourne le chemin du répertoire racine. + * @return Chemin absolu du répertoire racine ("/"). + */ + public String getRoot() { return FileSystem.ROOT; } + + /** + * Retourne le répertoire courant. + * @return Chemin absolu du répertoire courant. + */ + public String getCurrentDirectory() { return this.currentDirectory; } + + /** + * Change le répertoire courant. + * @param path Chemin absolu du nouveau répertoire courant. + * @throws IllegalArgumentException Si le répertoire n'existe pas. + */ + public void setCurrentDirectory(String path) { + if (!this.directories.containsKey(path)) { + throw new IllegalArgumentException("Répertoire introuvable: " + path); + } + this.currentDirectory = path; + } + + // ============================================ + // Méthodes pour les répertoires + // ============================================ + + /** + * Crée un nouveau répertoire dans le répertoire courant. + * @param dirName Nom du répertoire à créer. + * @return true si le répertoire a été créé, false s'il existe déjà. + * @throws IllegalArgumentException Si dirName est vide ou null. + */ + public boolean mkdir(String dirName) { + if (dirName == null || dirName.trim().isEmpty()) { + throw new IllegalArgumentException("Nom de répertoire invalide."); + } + + String fullPath = getFullPath(dirName); + + // Vérifier si le répertoire existe déjà + if (this.directories.containsKey(fullPath)) { + return false; + } + + // Créer le répertoire + this.directories.put(fullPath, new HashSet<>()); + + // Ajouter le répertoire au répertoire parent + this.directories.get(this.currentDirectory).add(dirName); + + return true; + } + + /** + * Supprime un répertoire ou un fichier. + * @param name Nom du répertoire ou fichier à supprimer. + * @return true si la suppression a réussi, false sinon. + */ + public boolean rm(String name) { + if (name == null || name.trim().isEmpty()) { + return false; + } + + String fullPath = getFullPath(name); + + // Supprimer un fichier + if (this.files.containsKey(fullPath)) { + this.files.remove(fullPath); + this.directories.get(this.currentDirectory).remove(name); + return true; + } + // Supprimer un répertoire (doit être vide) + else if (this.directories.containsKey(fullPath)) { + Set contents = this.directories.get(fullPath); + if (contents.isEmpty()) { + this.directories.remove(fullPath); + this.directories.get(this.currentDirectory).remove(name); + return true; + } else { + return false; // Répertoire non vide + } + } + + return false; + } + + /** + * Supprime un répertoire et tout son contenu (récursif). + * @param name Nom du répertoire à supprimer. + * @return true si la suppression a réussi, false sinon. + */ + public boolean rmdir(String name) { + if (name == null || name.trim().isEmpty()) { + return false; + } + + String fullPath = getFullPath(name); + + if (!this.directories.containsKey(fullPath)) { + return false; + } + + // Supprimer récursivement le contenu + Set contents = new HashSet<>(this.directories.get(fullPath)); + for (String item : contents) { + String itemPath = fullPath.equals(ROOT) ? ROOT + item : fullPath + "/" + item; + if (this.directories.containsKey(itemPath)) { + rmdir(item); // Supprimer récursivement + } else if (this.files.containsKey(itemPath)) { + this.files.remove(itemPath); + } + } + + // Supprimer le répertoire lui-même + this.directories.remove(fullPath); + this.directories.get(this.currentDirectory).remove(name); + + return true; + } + + /** + * Liste le contenu du répertoire courant. + * @return Liste des noms de fichiers et répertoires (les répertoires se terminent par "/"). + */ + public List ls() { + List contents = new ArrayList(); + Set dirContents = this.directories.getOrDefault(this.currentDirectory, new HashSet<>()); + + for (String item : dirContents) { + String fullPath = getFullPath(item); + if (this.directories.containsKey(fullPath)) { + contents.add(item + "/"); // Ajouter "/" pour les répertoires + } else if (this.files.containsKey(fullPath)) { + contents.add(item); + } + } + + Collections.sort(contents); // Trier par ordre alphabétique + return contents; + } + + /** + * Liste le contenu d'un répertoire spécifique. + * @param dirPath Chemin du répertoire à lister. + * @return Liste des noms de fichiers et répertoires. + * @throws IllegalArgumentException Si le répertoire n'existe pas. + */ + public List ls(String dirPath) { + if (!this.directories.containsKey(dirPath)) { + throw new IllegalArgumentException("Répertoire introuvable: " + dirPath); + } + + List contents = new ArrayList<>(); + Set dirContents = this.directories.get(dirPath); + + for (String item : dirContents) { + String fullPath = dirPath.equals(ROOT) ? ROOT + item : dirPath + "/" + item; + if (this.directories.containsKey(fullPath)) { + contents.add(item + "/"); + } else if (this.files.containsKey(fullPath)) { + contents.add(item); + } + } + + Collections.sort(contents); + return contents; + } + + // ============================================ + // Méthodes pour les fichiers + // ============================================ + + /** + * Crée un nouveau fichier vide. + * @param fileName Nom du fichier à créer. + * @return true si le fichier a été créé, false s'il existe déjà. + * @throws IllegalArgumentException Si fileName est vide ou null. + */ + public boolean touch(String fileName) { + if (fileName == null || fileName.trim().isEmpty()) { + throw new IllegalArgumentException("Nom de fichier invalide."); + } + + String fullPath = getFullPath(fileName); + + if (this.files.containsKey(fullPath)) { + return false; // Fichier déjà existant + } + + this.files.put(fullPath, ""); + return true; + } + + /** + * Lit le contenu d'un fichier. + * @param fileName Nom du fichier à lire. + * @return Contenu du fichier, ou un message d'erreur si le fichier n'existe pas. + */ + public String cat(String fileName) { + String fullPath = getFullPath(fileName); + return this.files.getOrDefault(fullPath, "Fichier introuvable: " + fileName); + } + + /** + * Écrit du contenu dans un fichier (écrasement). + * @param fileName Nom du fichier. + * @param content Contenu à écrire. + * @return true si l'écriture a réussi, false sinon. + */ + public boolean echo(String fileName, String content) { + if (fileName == null || fileName.trim().isEmpty()) { + return false; + } + + String fullPath = getFullPath(fileName); + this.files.put(fullPath, content); + return true; + } + + /** + * Ajoute du contenu à la fin d'un fichier. + * @param fileName Nom du fichier. + * @param content Contenu à ajouter. + * @return true si l'ajout a réussi, false sinon. + */ + public boolean append(String fileName, String content) { + if (fileName == null || fileName.trim().isEmpty()) { + return false; + } + + String fullPath = getFullPath(fileName); + String existingContent = this.files.getOrDefault(fullPath, ""); + this.files.put(fullPath, existingContent + content); + return true; + } + + // ============================================ + // Méthodes utilitaires pour les chemins + // ============================================ + + /** + * Construit le chemin absolu à partir d'un nom de fichier/répertoire. + * @param name Nom du fichier ou répertoire. + * @return Chemin absolu. + */ + private String getFullPath(String name) { + if (this.currentDirectory.equals(ROOT)) { + return ROOT + name; + } + return this.currentDirectory + "/" + name; + } + + /** + * Vérifie si un fichier ou répertoire existe. + * @param name Nom du fichier ou répertoire. + * @return true s'il existe, false sinon. + */ + public boolean exists(String name) { + String fullPath = getFullPath(name); + return this.files.containsKey(fullPath) || this.directories.containsKey(fullPath); + } + + /** + * Retourne le chemin absolu du répertoire parent. + * @param path Chemin dont on veut le parent. + * @return Chemin absolu du répertoire parent. + */ + public String getParentDirectory(String path) { + if (path.equals(ROOT)) { + return ROOT; + } + + String[] parts = path.split("/"); + if (parts.length <= 2) { + return ROOT; + } + + StringBuilder parentPath = new StringBuilder(); + for (int i = 1; i < parts.length - 1; i++) { + parentPath.append("/").append(parts[i]); + } + + return parentPath.length() == 0 ? ROOT : parentPath.toString(); + } + + /** + * Résout un chemin (relatif ou absolu) en chemin absolu. + * @param path Chemin à résoudre (peut être relatif ou absolu). + * @return Chemin absolu résolu. + * @throws IllegalArgumentException Si le chemin est invalide. + */ + public String resolvePath(String path) { + if (path == null || path.trim().isEmpty()) { + return this.currentDirectory; + } + + path = path.trim(); + + // Si le chemin est absolu (commence par "/") + if (path.startsWith(ROOT)) { + return normalizePath(path); + } + + // Sinon, c'est un chemin relatif : le résoudre par rapport au répertoire courant + String resolvedPath = this.currentDirectory + "/" + path; + return normalizePath(resolvedPath); + } + + /** + * Normalise un chemin (supprime les "/./" et résout les "/../"). + * @param path Chemin à normaliser. + * @return Chemin normalisé. + */ + public String normalizePath(String path) { + if (path.equals(ROOT)) { + return ROOT; + } + + String[] parts = path.split("/"); + List normalizedParts = new ArrayList<>(); + + for (String part : parts) { + if (part.isEmpty() || part.equals(".")) { + continue; // Ignorer les parties vides ou "." + } else if (part.equals("..")) { + if (!normalizedParts.isEmpty()) { + normalizedParts.remove(normalizedParts.size() - 1); // Remonter d'un niveau + } + } else { + normalizedParts.add(part); + } + } + + if (normalizedParts.isEmpty()) { + return ROOT; + } + + StringBuilder normalizedPath = new StringBuilder(); + for (String part : normalizedParts) { + normalizedPath.append("/").append(part); + } + + return normalizedPath.toString(); + } + + // ============================================ + // Méthodes pour la gestion des permissions (optionnel) + // ============================================ + + /** + * Vérifie si un chemin est un répertoire. + * @param path Chemin à vérifier. + * @return true si c'est un répertoire, false sinon. + */ + public boolean isDirectory(String path) { + return this.directories.containsKey(path); + } + + /** + * Vérifie si un chemin est un fichier. + * @param path Chemin à vérifier. + * @return true si c'est un fichier, false sinon. + */ + public boolean isFile(String path) { + return this.files.containsKey(path); + } + + // ============================================ + // Méthodes pour la sérialisation (optionnel) + // ============================================ + + /** + * Exporte l'état du système de fichiers sous forme de chaîne de caractères. + * Utile pour sauvegarder/restaurer l'état. + * @return Représentation textuelle du système de fichiers. + */ + public String exportState() { + StringBuilder sb = new StringBuilder(); + sb.append("=== Directories ===\\n"); + for (Map.Entry> entry : this.directories.entrySet()) { + sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\\n"); + } + sb.append("\\n=== Files ===\\n"); + for (Map.Entry entry : this.files.entrySet()) { + sb.append(entry.getKey()).append(": \"").append(entry.getValue()).append("\"\\n"); + } + return sb.toString(); + } + + /** + * Importe l'état du système de fichiers à partir d'une chaîne de caractères. + * @param state Représentation textuelle du système de fichiers. + */ + public void importState(String state) { + // TODO: Implémenter la désérialisation + throw new UnsupportedOperationException("Non implémenté."); + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/TerminalDirectory.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/TerminalDirectory.java new file mode 100644 index 0000000..a161097 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/TerminalDirectory.java @@ -0,0 +1,115 @@ +package gabywald.terminal.filesystem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Directory class representing a folder in the file system. + * @author Gabriel Chandesris (2026) + */ +public class TerminalDirectory extends TerminalNode { + private List children; + + public TerminalDirectory(String name, TerminalDirectory parent) { + super(name, parent); + this.children = new ArrayList(); + } + + public List getChildren() { return Collections.unmodifiableList(children); } + + public boolean addChild(TerminalNode child) { + if (child == null) { return false; } + for (TerminalNode existing : children) { + if (existing.getName().equals(child.getName())) + { return false; } + } + child.setParent(this); + this.children.add(child); + this.modifiedAt = java.time.LocalDateTime.now(); + return true; + } + + public boolean removeChild(TerminalNode child) { + if (child == null) { return false; } + boolean removed = this.children.remove(child); + if (removed) { + child.setParent(null); + this.modifiedAt = java.time.LocalDateTime.now(); + } + return removed; + } + + public boolean removeChildByName(String name) { + TerminalNode toRemove = getChild(name); + if (toRemove != null) { return removeChild(toRemove); } + return false; + } + + public TerminalNode getChild(String name) { + for (TerminalNode child : children) { + if (child.getName().equals(name)) + { return child; } + } + return null; + } + + public boolean hasChild(String name) { return getChild(name) != null; } + + public List getSubdirectories() { + return this.children.stream() + .filter(TerminalNode::isDirectory) + .map(node -> (TerminalDirectory) node) + .collect(Collectors.toList()); + } + + public List getFiles() { + return this.children.stream() + .filter(TerminalNode::isFile) + .map(node -> (TerminalFile) node) + .collect(Collectors.toList()); + } + + @Override + public String getPath() { + if (this.parent == null) { return "/"; } + String parentPath = parent.getPath(); + if ("/".equals(parentPath)) return "/" + this.name; + return parentPath + "/" + this.name; + } + + @Override + public boolean isDirectory() { return true; } + @Override + public boolean isFile() { return false; } + @Override + public long getSize() { return 0; } + + @Override + public boolean delete() { + if (this.parent != null) return this.parent.removeChild(this); + return false; + } + + public void clear() { this.children.clear(); this.modifiedAt = java.time.LocalDateTime.now(); } + public int getChildCount() { return this.children.size(); } + public boolean isEmpty() { return this.children.isEmpty(); } + + public TerminalDirectory createDirectory(String name) { + if (this.hasChild(name)) { return null; } + TerminalDirectory dir = new TerminalDirectory(name, this); + this.addChild(dir); + return dir; + } + + public TerminalFile createFile(String name) { + if (this.hasChild(name)) { return null; } + TerminalFile file = new TerminalFile(name, this); + this.addChild(file); + return file; + } + + @Override + public String toString() { return "[DIR] " + this.name + " (" + this.children.size() + " items)"; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/TerminalFile.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/TerminalFile.java new file mode 100644 index 0000000..e70a299 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/TerminalFile.java @@ -0,0 +1,60 @@ +package gabywald.terminal.filesystem; + +/** + * TerminalFile class representing a text file in the file system. + * @author Gabriel Chandesris (2026) + */ +public class TerminalFile extends TerminalNode { + private String content; + + public TerminalFile(String name, TerminalDirectory parent) { + super(name, parent); + this.content = ""; + } + + public TerminalFile(String name, TerminalDirectory parent, String content) { + super(name, parent); + this.content = ((content != null) ? content : ""); + } + + public String getContent() { return content; } + + public void setContent(String content) { + this.content = content != null ? content : ""; + this.modifiedAt = java.time.LocalDateTime.now(); + } + + public void appendContent(String text) { + if (text != null) { + this.content += text; + this.modifiedAt = java.time.LocalDateTime.now(); + } + } + + @Override + public String getPath() { + if (parent == null) { return "/" + name; } + String parentPath = parent.getPath(); + if ("/".equals(parentPath)) { return "/" + name; } + return parentPath + "/" + name; + } + + @Override + public boolean isDirectory() { return false; } + @Override + public boolean isFile() { return true; } + @Override + public long getSize() { return content.getBytes().length; } + + @Override + public boolean delete() { + if (parent != null) { return parent.removeChild(this); } + return false; + } + + public void clear() { this.content = ""; this.modifiedAt = java.time.LocalDateTime.now(); } + public boolean isEmpty() { return content.isEmpty(); } + + @Override + public String toString() { return "[FILE] " + name + " (" + getSize() + " bytes)"; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/TerminalNode.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/TerminalNode.java new file mode 100644 index 0000000..423b1e4 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/filesystem/TerminalNode.java @@ -0,0 +1,58 @@ +package gabywald.terminal.filesystem; + +import java.time.LocalDateTime; + +/** + * Abstract base class for file system nodes (files and directories). + * @author Gabriel Chandesris (2026) + */ +public abstract class TerminalNode implements Comparable { + + protected String name; + protected TerminalDirectory parent; + protected LocalDateTime createdAt; + protected LocalDateTime modifiedAt; + + public TerminalNode(String name, TerminalDirectory parent) { + this.name = name; + this.parent = parent; + this.createdAt = LocalDateTime.now(); + this.modifiedAt = LocalDateTime.now(); + } + + public String getName() { return name; } + public void setName(String name) { this.name = name; this.modifiedAt = LocalDateTime.now(); } + public TerminalDirectory getParent() { return parent; } + public void setParent(TerminalDirectory parent) { this.parent = parent; this.modifiedAt = LocalDateTime.now(); } + public LocalDateTime getCreatedAt() { return createdAt; } + public LocalDateTime getModifiedAt() { return modifiedAt; } + + public abstract String getPath(); + public abstract boolean isDirectory(); + public abstract boolean isFile(); + public abstract long getSize(); + public abstract boolean delete(); + + @Override + public String toString() { return name; } + + @Override + public int compareTo(TerminalNode other) { return this.name.compareTo(other.name); } + + @Override + public boolean equals(Object obj) { + if (this == obj) { return true; } + if (obj == null || this.getClass() != obj.getClass()) { return false;} + TerminalNode fileNode = (TerminalNode) obj; + return name.equals(fileNode.name) && + ( (parent == null) ? fileNode.parent == null : parent.equals(fileNode.parent)); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + ( (parent != null) ? parent.hashCode() : 0); + return result; + } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/CommandActionListener.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/CommandActionListener.java new file mode 100644 index 0000000..e28cad2 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/CommandActionListener.java @@ -0,0 +1,60 @@ +package gabywald.terminal.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import gabywald.terminal.commands.Command; +import gabywald.terminal.commands.CommandFactory; +import gabywald.terminal.commands.CommandParser; + +/** + * + * @author Gabriel Chandesris (2026) + */ +public class CommandActionListener implements ActionListener { + private TerminalFrame localtf = null; + + CommandActionListener(TerminalFrame tf) + { this.localtf = tf; } + + @Override + public void actionPerformed(ActionEvent e) { + String command = this.localtf.getInputField().getText().trim(); + if (command.isEmpty()) { + this.localtf.clearInputLine(); + return; + } + + this.localtf.getTerminalState().addToHistory(command); + this.localtf.getTerminalState().resetHistoryIndex(); + this.localtf.appendOutput(this.localtf.getTerminalState().getPrompt() + " " + command + "\n"); + + String[] parts = CommandParser.parse(command); + if (parts.length == 0) { + this.localtf.clearInputLine(); + return; + } + + String commandName = parts[0]; + String[] cmdArgs = new String[parts.length - 1]; + System.arraycopy(parts, 1, cmdArgs, 0, cmdArgs.length); + + Command cmd = CommandFactory.getCommand(commandName); + if (cmd == null) { + this.localtf.appendOutput(commandName + ": command not found\n"); + } else { + String result = cmd.execute(this.localtf.getTerminalState(), cmdArgs); + if ("EXIT".equals(result)) { + System.exit(0); + } else if (result.contains("\033[H\033[2J")) { + this.localtf.clearScreen(); + this.localtf.printWelcomeMessage(); + } else { + this.localtf.appendOutput(result + "\n"); + } + } + + this.localtf.updatePrompt(); + this.localtf.clearInputLine(); + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/HistoryKeyListener.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/HistoryKeyListener.java new file mode 100644 index 0000000..50d3b77 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/HistoryKeyListener.java @@ -0,0 +1,52 @@ +package gabywald.terminal.gui; + +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import gabywald.terminal.commands.CommandFactory; +import gabywald.terminal.commands.CommandParser; + +/** + * + * @author Gabriel Chandesris (2026) + */ +public class HistoryKeyListener extends KeyAdapter { + + private TerminalFrame localtf = null; + + HistoryKeyListener(TerminalFrame tf) + { this.localtf = tf; } + + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_UP) { + String prevCommand = this.localtf.getTerminalState().getPreviousCommand(); + if (prevCommand != null) { this.localtf.getInputField().setText(prevCommand); } + e.consume(); + } else if (e.getKeyCode() == KeyEvent.VK_DOWN) { + String nextCommand = this.localtf.getTerminalState().getNextCommand(); + if (nextCommand != null) { this.localtf.getInputField().setText(nextCommand); } + e.consume(); + } else if (e.getKeyCode() == KeyEvent.VK_TAB) { + handleTabCompletion(); + e.consume(); + } + } + + private void handleTabCompletion() { + String text = this.localtf.getInputField().getText(); + String[] parts = CommandParser.parse(text); + if (parts.length == 0) { return; } + + if (parts.length == 1) { + String prefix = parts[0]; + String[] commandNames = CommandFactory.getCommandNames(); + for (String cmdName : commandNames) { + if (cmdName.startsWith(prefix)) { + this.localtf.getInputField().setText(cmdName + " "); + return; + } + } + } + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/TerminalFrame.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/TerminalFrame.java new file mode 100644 index 0000000..127f9c4 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/TerminalFrame.java @@ -0,0 +1,118 @@ +package gabywald.terminal.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; + +import javax.swing.BorderFactory; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingConstants; + +import gabywald.terminal.TerminalState; + + +/** + * Main terminal window with Swing GUI + * @author Gabriel Chandesris (2026) + */ +public class TerminalFrame extends JFrame { + + private static final long serialVersionUID = 1L; + + private TerminalState state; + private JTextArea outputArea; + private JTextField inputField; + private JScrollPane scrollPane; + private JLabel promptLabel; + + public TerminalFrame() { + this(new TerminalState()); + } + + public TerminalFrame(TerminalState state) { + this.state = state; + this.initializeUI(); + } + + private void initializeUI() { + this.setTitle("Terminal Emulator"); + this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + this.setSize(800, 600); + this.setLocationRelativeTo(null); + + JPanel mainPanel = new JPanel(new BorderLayout()); + mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + this.outputArea = new JTextArea(); + this.outputArea.setEditable(false); + this.outputArea.setFont(new Font("Monospaced", Font.PLAIN, 14)); + this.outputArea.setBackground(Color.BLACK); + this.outputArea.setForeground(Color.WHITE); + this.outputArea.setCaretColor(Color.WHITE); + + this.scrollPane = new JScrollPane(this.outputArea); + this.scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + this.scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + + mainPanel.add(this.scrollPane, BorderLayout.CENTER); + + JPanel inputPanel = new JPanel(new BorderLayout()); + inputPanel.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0)); + + this.promptLabel = new JLabel(this.state.getPrompt() + " ", SwingConstants.RIGHT); + this.promptLabel.setFont(new Font("Monospaced", Font.PLAIN, 14)); + this.promptLabel.setForeground(Color.WHITE); + this.promptLabel.setBackground(Color.BLACK); + this.promptLabel.setOpaque(true); + + this.inputField = new JTextField(); + this.inputField.setFont(new Font("Monospaced", Font.PLAIN, 14)); + this.inputField.setBackground(Color.BLACK); + this.inputField.setForeground(Color.WHITE); + this.inputField.setCaretColor(Color.WHITE); + this.inputField.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5)); + + this.inputField.addActionListener(new CommandActionListener(this)); + this.inputField.addKeyListener(new HistoryKeyListener(this)); + + inputPanel.add(this.promptLabel, BorderLayout.WEST); + inputPanel.add(this.inputField, BorderLayout.CENTER); + + mainPanel.add(inputPanel, BorderLayout.SOUTH); + + this.add(mainPanel); + + this.printWelcomeMessage(); + this.inputField.requestFocusInWindow(); + } + + void printWelcomeMessage() { + this.appendOutput("===============================================\n"); + this.appendOutput(" TERMINAL EMULATOR - Java 8 / Swing\n"); + this.appendOutput(" Type 'help' for a list of available commands\n"); + this.appendOutput(" Type 'exit' to quit\n"); + this.appendOutput("===============================================\n\n"); + } + + void appendOutput(String text) { + this.outputArea.append(text); + this.outputArea.setCaretPosition(outputArea.getDocument().getLength()); + } + + void clearScreen() { this.outputArea.setText(""); } + + void clearInputLine() { this.inputField.setText(""); } + + void updatePrompt() { this.promptLabel.setText(this.state.getPrompt() + " "); } + + JTextField getInputField() { return this.inputField; } + + public TerminalState getTerminalState() { return this.state; } + public void setState(TerminalState state) { this.state = state;this.updatePrompt(); } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/TerminalOutput.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/TerminalOutput.java new file mode 100644 index 0000000..5ae3979 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/TerminalOutput.java @@ -0,0 +1,80 @@ +package gabywald.terminal.gui; + +import java.awt.Color; +import java.awt.Font; + +import javax.swing.JTextPane; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultStyledDocument; +import javax.swing.text.Document; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyleContext; + +/** + * Terminal output with color support + * @author Gabriel Chandesris (2026) + */ +public class TerminalOutput { + + private JTextPane textPane; + private StyleContext styleContext; + private Style defaultStyle; + private Style errorStyle; + private Style successStyle; + private Style promptStyle; + private Style commandStyle; + + public TerminalOutput() { + this.textPane = new JTextPane(); + this.textPane.setEditable(false); + this.textPane.setFont(new Font("Monospaced", Font.PLAIN, 14)); + this.textPane.setBackground(Color.BLACK); + this.textPane.setCaretColor(Color.WHITE); + + this.styleContext = new StyleContext(); + + this.defaultStyle = this.styleContext.addStyle("default", null); + StyleConstants.setForeground(this.defaultStyle, Color.WHITE); + + this.errorStyle = this.styleContext.addStyle("error", null); + StyleConstants.setForeground(this.errorStyle, Color.RED); + + this.successStyle = this.styleContext.addStyle("success", null); + StyleConstants.setForeground(this.successStyle, Color.GREEN); + + this.promptStyle = this.styleContext.addStyle("prompt", null); + StyleConstants.setForeground(promptStyle, Color.CYAN); + + commandStyle = styleContext.addStyle("command", null); + StyleConstants.setForeground(this.commandStyle, Color.YELLOW); + + this.textPane.setDocument(new DefaultStyledDocument(styleContext)); + } + + public void append(String text) { this.append(text, this.defaultStyle); } + + public void append(String text, Style style) { + try { + Document doc = this.textPane.getDocument(); + doc.insertString(doc.getLength(), text, style); + this.textPane.setCaretPosition(doc.getLength()); + } catch (BadLocationException e) { e.printStackTrace(); } + } + + public void appendLine(String text) { this.append(text + "\n"); } + public void appendLine(String text, Style style) { this.append(text + "\n", style); } + public void appendPrompt(String prompt) { this.append(prompt + " ", this.promptStyle); } + public void appendCommand(String command) { this.append(command, this.commandStyle); } + public void appendError(String error) { this.appendLine(error, this.errorStyle); } + public void appendSuccess(String message) { this.appendLine(message, this.successStyle); } + public void clearScreen() { this.textPane.setText(""); } + + public JTextPane getTextPane() { return this.textPane; } + public Style getDefaultStyle() { return this.defaultStyle; } + public Style getErrorStyle() { return this.errorStyle; } + public Style getSuccessStyle() { return this.successStyle; } + public Style getPromptStyle() { return this.promptStyle; } + public Style getCommandStyle() { return this.commandStyle; } + +} \ No newline at end of file diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/TerminalPanel.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/TerminalPanel.java new file mode 100644 index 0000000..0956e88 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/gui/TerminalPanel.java @@ -0,0 +1,51 @@ +package gabywald.terminal.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; + +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +/** + * Terminal panel with output display + * @author Gabriel Chandesris (2026) + */ +public class TerminalPanel extends JPanel { + private static final long serialVersionUID = 1L; + private JTextArea outputArea; + private JScrollPane scrollPane; + + public TerminalPanel() { + setLayout(new BorderLayout()); + setBackground(Color.BLACK); + + this.outputArea = new JTextArea(); + this.outputArea.setEditable(false); + this.outputArea.setFont(new Font("Monospaced", Font.PLAIN, 14)); + this.outputArea.setBackground(Color.BLACK); + this.outputArea.setForeground(Color.WHITE); + this.outputArea.setCaretColor(Color.WHITE); + + this.scrollPane = new JScrollPane(this.outputArea); + this.scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + this.scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + this.scrollPane.setBorder(BorderFactory.createEmptyBorder()); + + this.add(this.scrollPane, BorderLayout.CENTER); + } + + public void appendOutput(String text) { + this.outputArea.append(text); + this.outputArea.setCaretPosition(this.outputArea.getDocument().getLength()); + } + + public void clearScreen() { this.outputArea.setText(""); } + public void setOutputText(String text) { this.outputArea.setText(text); } + public String getOutputText() { return this.outputArea.getText(); } + public JTextArea getOutputArea() { return this.outputArea; } + +} + diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/script/ScriptContext.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/script/ScriptContext.java new file mode 100644 index 0000000..7e80d44 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/script/ScriptContext.java @@ -0,0 +1,96 @@ +package gabywald.terminal.script; + +import java.util.HashMap; +import java.util.Map; + +/** + * Script execution context with variables and control flow state + * @author Gabriel Chandesris (2026) + */ +public class ScriptContext { + private Map variables; + private boolean inIfBlock; + private boolean ifConditionResult; + private int ifBlockDepth; + private boolean inWhileBlock; + private String whileCondition; + private boolean whileConditionResult; + private int whileBlockDepth; + private boolean inForBlock; + private String forVariable; + private String[] forValues; + private int forIndex; + private int forBlockDepth; + + public ScriptContext() + { this.variables = new HashMap(); } + + public Map getVariables() { return this.variables; } + public void setVariables(Map variables) + { this.variables = variables; } + + public boolean isInIfBlock() { return this.inIfBlock; } + public void setInIfBlock(boolean inIfBlock) + { this.inIfBlock = inIfBlock; } + + public boolean isIfConditionResult() { return this.ifConditionResult; } + public void setIfConditionResult(boolean ifConditionResult) + { this.ifConditionResult = ifConditionResult; } + + public int getIfBlockDepth() { return this.ifBlockDepth; } + public void setIfBlockDepth(int ifBlockDepth) + { this.ifBlockDepth = ifBlockDepth; } + + public boolean isInWhileBlock() { return this.inWhileBlock; } + public void setInWhileBlock(boolean inWhileBlock) + { this.inWhileBlock = inWhileBlock; } + + public String getWhileCondition() { return this.whileCondition; } + public void setWhileCondition(String whileCondition) + { this.whileCondition = whileCondition; } + + public boolean isWhileConditionResult() { return this.whileConditionResult; } + public void setWhileConditionResult(boolean whileConditionResult) + { this.whileConditionResult = whileConditionResult; } + + public int getWhileBlockDepth() { return this.whileBlockDepth; } + public void setWhileBlockDepth(int whileBlockDepth) + { this.whileBlockDepth = whileBlockDepth; } + + public boolean isInForBlock() { return this.inForBlock; } + public void setInForBlock(boolean inForBlock) + { this.inForBlock = inForBlock; } + + public String getForVariable() { return this.forVariable; } + public void setForVariable(String forVariable) + { this.forVariable = forVariable; } + + public String[] getForValues() { return this.forValues; } + public void setForValues(String[] forValues) + { this.forValues = forValues; } + + public int getForIndex() { return this.forIndex; } + public void setForIndex(int forIndex) + { this.forIndex = forIndex; } + + public int getForBlockDepth() { return this.forBlockDepth; } + public void setForBlockDepth(int forBlockDepth) + { this.forBlockDepth = forBlockDepth; } + + public void reset() { + this.variables.clear(); + this.inIfBlock = false; + this.ifConditionResult = false; + this.ifBlockDepth = 0; + this.inWhileBlock = false; + this.whileCondition = ""; + this.whileConditionResult = false; + this.whileBlockDepth = 0; + this.inForBlock = false; + this.forVariable = ""; + this.forValues = new String[0]; + this.forIndex = 0; + this.forBlockDepth = 0; + } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/script/ScriptEngine.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/script/ScriptEngine.java new file mode 100644 index 0000000..82cb410 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/script/ScriptEngine.java @@ -0,0 +1,263 @@ +package gabywald.terminal.script; + +import gabywald.terminal.TerminalState; +import gabywald.terminal.commands.Command; +import gabywald.terminal.commands.CommandFactory; +import gabywald.terminal.commands.CommandParser; +import gabywald.terminal.filesystem.TerminalDirectory; +import gabywald.terminal.filesystem.TerminalNode; +import gabywald.terminal.filesystem.TerminalFile; +import java.util.Map; + +/** + * Script execution engine with minimal scripting language support + * @author Gabriel Chandesris (2026) + */ +public class ScriptEngine { + + private TerminalState state; + private ScriptContext context; + + public ScriptEngine(TerminalState state) { + this.state = state; + this.context = new ScriptContext(); + } + + public String executeFile(TerminalFile file) { + if (file == null) { return "Script file not found"; } + return execute(file.getContent()); + } + + public String execute(String script) { + if (script == null || script.trim().isEmpty()) { return ""; } + + StringBuilder output = new StringBuilder(); + String[] lines = script.split("\n"); + + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.startsWith("#") || trimmed.isEmpty()) { continue; } + + String result = executeLine(trimmed); + if (result != null && !result.isEmpty()) + { output.append(result).append("\n"); } + } + return output.toString(); + } + + private String executeLine(String line) { + if (line.startsWith("if ")) return executeIf(line); + if (line.startsWith("while ")) return executeWhile(line); + if (line.startsWith("for ")) return executeFor(line); + if (line.equals("fi") || line.equals("done") || line.equals("end")) return null; + if (line.startsWith("set ")) return executeSet(line); + if (line.startsWith("echo ")) return executeScriptEcho(line); + if (line.startsWith("cd ")) return executeScriptCd(line); + if (line.startsWith("pwd")) return state.getCurrentDirectory().getPath(); + if (line.startsWith("ls")) return executeLs(line); + if (line.startsWith("cat")) return executeCat(line); + + String[] parts = CommandParser.parse(line); + if (parts.length > 0) { + Command cmd = CommandFactory.getCommand(parts[0]); + if (cmd != null) { + String[] args = new String[parts.length - 1]; + System.arraycopy(parts, 1, args, 0, args.length); + return cmd.execute(state, args); + } + } + return "Script error: unknown command '" + line.split(" ")[0] + "'"; + } + + private String executeIf(String line) { + String condition = line.substring(3, line.length() - 1).trim(); + boolean result = evaluateCondition(condition); + context.setInIfBlock(true); + context.setIfConditionResult(result); + context.setIfBlockDepth(1); + return null; + } + + private String executeWhile(String line) { + String condition = line.substring(6, line.length() - 1).trim(); + boolean result = evaluateCondition(condition); + context.setInWhileBlock(true); + context.setWhileCondition(condition); + context.setWhileConditionResult(result); + context.setWhileBlockDepth(1); + return null; + } + + private String executeFor(String line) { + String[] parts = line.substring(4).trim().split(" in "); + if (parts.length != 2) { return "Script error: invalid for syntax"; } + + String varName = parts[0].trim(); + String[] values = parts[1].trim().split(" "); + + context.setInForBlock(true); + context.setForVariable(varName); + context.setForValues(values); + context.setForIndex(0); + context.setForBlockDepth(1); + + if (values.length > 0) { context.getVariables().put(varName, values[0]); } + return null; + } + + private String executeSet(String line) { + String[] parts = line.substring(4).trim().split("=", 2); + if (parts.length != 2) { return "Script error: invalid set syntax"; } + + String varName = parts[0].trim(); + String value = parts[1].trim(); + value = replaceVariables(value); + context.getVariables().put(varName, value); + return null; + } + + private String executeScriptEcho(String line) { + String text = line.substring(5).trim(); + return replaceVariables(text); + } + + private String executeScriptCd(String line) { + String path = line.substring(3).trim(); + TerminalDirectory newDir = resolveDirectory(state, path); + if (newDir == null) { return "cd: no such file or directory: " + path; } + state.setCurrentDirectory(newDir); + return null; + } + + private String executeLs(String line) { + String[] parts = CommandParser.parse(line); + String[] args = new String[parts.length - 1]; + System.arraycopy(parts, 1, args, 0, args.length); + Command cmd = CommandFactory.getCommand("ls"); + if (cmd != null) { return cmd.execute(state, args); } + return "ls: command not available"; + } + + private String executeCat(String line) { + String[] parts = CommandParser.parse(line); + String[] args = new String[parts.length - 1]; + System.arraycopy(parts, 1, args, 0, args.length); + Command cmd = CommandFactory.getCommand("cat"); + if (cmd != null) { return cmd.execute(state, args); } + return "cat: command not available"; + } + + private boolean evaluateCondition(String condition) { + condition = condition.trim(); + if (condition.startsWith("$")) { + String[] parts = condition.split("="); + if (parts.length == 2) { + String varName = parts[0].trim(); + String value = parts[1].trim().replaceAll("^$", ""); // NOTE "^\"|"$" + String varValue = context.getVariables().getOrDefault(varName, ""); + return varValue.equals(value); + } + } else if (condition.startsWith("-")) { + String[] parts = condition.split(" "); + if (parts.length == 2) { + String test = parts[0]; + String path = parts[1].trim().replaceAll("^$", ""); // NOTE "^\"|"$" + TerminalNode node = resolveFile(state, path); + if ("-f".equals(test)) { return node != null && node.isFile(); } + if ("-d".equals(test)) { return node != null && node.isDirectory(); } + if ("-e".equals(test)) { return node != null; } + } + } + return false; + } + + private String replaceVariables(String text) { + String result = text; + for (Map.Entry entry : context.getVariables().entrySet()) + { result = result.replace("$" + entry.getKey(), entry.getValue()); } + return result; + } + + private TerminalNode resolveFile(TerminalState state, String fileName) { + TerminalDirectory current = state.getCurrentDirectory(); + if (fileName.startsWith("/")) { return resolveAbsolutePath(state.getRootDirectory(), fileName); } + return resolveRelativePath(current, fileName); + } + + private TerminalDirectory resolveDirectory(TerminalState state, String dirName) { + TerminalDirectory current = state.getCurrentDirectory(); + if (dirName.startsWith("/")) { return resolveAbsoluteDirectory(state.getRootDirectory(), dirName); } + return resolveRelativeDirectory(current, dirName); + } + + private TerminalNode resolveAbsolutePath(TerminalDirectory root, String path) { + String[] parts = path.split("/"); + TerminalDirectory current = root; + for (int i = 1; i < parts.length; i++) { + if (parts[i].isEmpty() || ".".equals(parts[i])) { continue; } + if ("..".equals(parts[i])) { + if (current.getParent() != null) { current = current.getParent(); } + } else { + TerminalNode node = current.getChild(parts[i]); + if (node == null) { return null; } + if (i == parts.length - 1) { return node; } + if (!node.isDirectory()) { return null; } + current = (TerminalDirectory) node; + } + } + return current; + } + + private TerminalNode resolveRelativePath(TerminalDirectory current, String path) { + String[] parts = path.split("/"); + for (int i = 0; i < parts.length; i++) { + if (parts[i].isEmpty() || ".".equals(parts[i])) { continue; } + if ("..".equals(parts[i])) { + if (current.getParent() != null) { current = current.getParent(); } + } else { + TerminalNode node = current.getChild(parts[i]); + if (node == null) { return null; } + if (i == parts.length - 1) { return node; } + if (!node.isDirectory()) { return null; } + current = (TerminalDirectory) node; + } + } + return current; + } + + private TerminalDirectory resolveAbsoluteDirectory(TerminalDirectory root, String path) { + String[] parts = path.split("/"); + TerminalDirectory current = root; + for (int i = 1; i < parts.length; i++) { + if (parts[i].isEmpty() || ".".equals(parts[i])) continue; + if ("..".equals(parts[i])) { + if (current.getParent() != null) current = current.getParent(); + } else { + TerminalNode node = current.getChild(parts[i]); + if (node == null || !node.isDirectory()) return null; + current = (TerminalDirectory) node; + } + } + return current; + } + + private TerminalDirectory resolveRelativeDirectory(TerminalDirectory current, String path) { + String[] parts = path.split("/"); + for (String part : parts) { + if (part.isEmpty() || ".".equals(part)) continue; + if ("..".equals(part)) { + if (current.getParent() != null) current = current.getParent(); + } else { + TerminalNode node = current.getChild(part); + if (node == null || !node.isDirectory()) return null; + current = (TerminalDirectory) node; + } + } + return current; + } + + public ScriptContext getContext() { return context; } + public TerminalState getState() { return state; } + public void setState(TerminalState state) { this.state = state; } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal/script/ScriptParser.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal/script/ScriptParser.java new file mode 100644 index 0000000..51b654d --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal/script/ScriptParser.java @@ -0,0 +1,94 @@ +package gabywald.terminal.script; + +import java.util.ArrayList; +import java.util.List; + +/** + * Script parser for syntax analysis + * @author Gabriel Chandesris (2026) + */ +public class ScriptParser { + + public static List parse(String script) { + List blocks = new ArrayList<>(); + if (script == null || script.trim().isEmpty()) { return blocks; } + + String[] lines = script.split("\n"); + ScriptBlock currentBlock = new ScriptBlock(ScriptBlock.Type.NORMAL, 0); + int depth = 0; + + for (int i = 0; i < lines.length; i++) { + String line = lines[i].trim(); + if (line.isEmpty() || line.startsWith("#")) { + if (currentBlock.getType() != ScriptBlock.Type.NORMAL) + { currentBlock.addLine(line); } + continue; + } + + if (line.startsWith("if ")) { + if (currentBlock.getType() != ScriptBlock.Type.NORMAL) { blocks.add(currentBlock); } + currentBlock = new ScriptBlock(ScriptBlock.Type.IF, i); + currentBlock.addLine(line); + depth++; + } else if (line.startsWith("while ")) { + if (currentBlock.getType() != ScriptBlock.Type.NORMAL) { blocks.add(currentBlock); } + currentBlock = new ScriptBlock(ScriptBlock.Type.WHILE, i); + currentBlock.addLine(line); + depth++; + } else if (line.startsWith("for ")) { + if (currentBlock.getType() != ScriptBlock.Type.NORMAL) { blocks.add(currentBlock); } + currentBlock = new ScriptBlock(ScriptBlock.Type.FOR, i); + currentBlock.addLine(line); + depth++; + } else if (line.equals("fi") || line.equals("done") || line.equals("end")) { + currentBlock.addLine(line); + blocks.add(currentBlock); + depth--; + if (depth >= 0) { currentBlock = new ScriptBlock(ScriptBlock.Type.NORMAL, i); } + } else { currentBlock.addLine(line); } + } + + if (!currentBlock.getLines().isEmpty()) { blocks.add(currentBlock); } + return blocks; + } + + public static boolean validateSyntax(String script) { + if (script == null) { return false; } + String[] lines = script.split("\n"); + int ifDepth = 0, whileDepth = 0, forDepth = 0; + + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.startsWith("if ")) ifDepth++; + else if (trimmed.equals("fi")) ifDepth--; + else if (trimmed.startsWith("while ")) whileDepth++; + else if (trimmed.equals("done")) { + if (whileDepth > 0) { whileDepth--; } + else if (forDepth > 0) { forDepth--; } + } + else if (trimmed.startsWith("for ")) forDepth++; + else if (trimmed.equals("end")) forDepth--; + } + return ifDepth == 0 && whileDepth == 0 && forDepth == 0; + } + + public static class ScriptBlock { + public enum Type { NORMAL, IF, WHILE, FOR } + private Type type; + private int startLine; + private List lines; + + public ScriptBlock(Type type, int startLine) { + this.type = type; + this.startLine = startLine; + this.lines = new ArrayList<>(); + } + + public void addLine(String line) { this.lines.add(line); } + public Type getType() { return this.type; } + public int getStartLine() { return this.startLine; } + public List getLines() { return this.lines; } + public String getContent() { return String.join("\n", this.lines); } + } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/FileSystem.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/FileSystem.java new file mode 100644 index 0000000..0fafcf1 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/FileSystem.java @@ -0,0 +1,330 @@ +package gabywald.terminal2; + +import java.util.*; + +/** + * Classe représentant un système de fichiers simplifié pour l'émulateur de terminal. + * Gère les fichiers et répertoires sous forme de structure arborescente. + * @author Gabriel Chandesris (2026) + */ +public class FileSystem { + private static final String ROOT = "/"; + private String currentDirectory; + private Map> directories; + private Map files; + + /** + * Constructeur : initialise le système de fichiers avec un répertoire racine. + */ + public FileSystem() { + this.currentDirectory = FileSystem.ROOT; + this.directories = new HashMap<>(); + this.files = new HashMap<>(); + this.directories.put(FileSystem.ROOT, new HashSet<>()); + } + + /** + * Retourne le répertoire racine. + */ + public String getRoot() { return FileSystem.ROOT; } + + /** + * Retourne le répertoire courant. + */ + public String getCurrentDirectory() { return this.currentDirectory; } + + /** + * Change le répertoire courant. + * @param path Chemin absolu du nouveau répertoire courant. + * @throws IllegalArgumentException Si le répertoire n'existe pas. + */ + public void setCurrentDirectory(String path) { + if (!this.directories.containsKey(path)) + { throw new IllegalArgumentException("Répertoire introuvable: " + path); } + this.currentDirectory = path; + } + + /** + * Crée un nouveau répertoire dans le répertoire courant. + * @param dirName Nom du répertoire à créer. + * @return true si le répertoire a été créé, false s'il existe déjà. + * @throws IllegalArgumentException Si dirName est vide ou null. + */ + public boolean mkdir(String dirName) { + if (dirName == null || dirName.trim().isEmpty()) + { throw new IllegalArgumentException("Nom de répertoire invalide."); } + + String fullPath = getFullPath(dirName); + + if (this.directories.containsKey(fullPath)) + { return false; } + + this.directories.put(fullPath, new HashSet<>()); + this.directories.get(this.currentDirectory).add(dirName); + return true; + } + + /** + * Supprime un fichier ou un répertoire (doit être vide). + * @param name Nom du fichier ou répertoire à supprimer. + * @return true si la suppression a réussi, false sinon. + */ + public boolean rm(String name) { + if (name == null || name.trim().isEmpty()) { return false; } + + String fullPath = this.getFullPath(name); + + if (this.files.containsKey(fullPath)) { + this.files.remove(fullPath); + this.directories.get(this.currentDirectory).remove(name); + return true; + } else if (this.directories.containsKey(fullPath)) { + Set contents = this.directories.get(fullPath); + if (contents.isEmpty()) { + this.directories.remove(fullPath); + this.directories.get(this.currentDirectory).remove(name); + return true; + } + return false; + } + + return false; + } + + /** + * Supprime un répertoire et tout son contenu (récursif). + * @param name Nom du répertoire à supprimer. + * @return true si la suppression a réussi, false sinon. + */ + public boolean rmdir(String name) { + if (name == null || name.trim().isEmpty()) { return false; } + + String fullPath = getFullPath(name); + + if (!this.directories.containsKey(fullPath)) { return false; } + + Set contents = new HashSet<>(this.directories.get(fullPath)); + for (String item : contents) { + String itemPath = fullPath.equals(ROOT) ? ROOT + item : fullPath + "/" + item; + if (this.directories.containsKey(itemPath)) { + rmdir(item); + } else if (this.files.containsKey(itemPath)) { + this.files.remove(itemPath); + } + } + + this.directories.remove(fullPath); + this.directories.get(this.currentDirectory).remove(name); + return true; + } + + /** + * Liste le contenu du répertoire courant. + * @return Liste des noms de fichiers et répertoires (les répertoires se terminent par "/"). + */ + public List ls() { + List contents = new ArrayList<>(); + Set dirContents = this.directories.getOrDefault(this.currentDirectory, new HashSet<>()); + + for (String item : dirContents) { + String fullPath = this.getFullPath(item); + if (this.directories.containsKey(fullPath)) { + contents.add(item + "/"); + } else if (this.files.containsKey(fullPath)) { + contents.add(item); + } + } + + Collections.sort(contents); + return contents; + } + + /** + * Liste le contenu d'un répertoire spécifique. + * @param dirPath Chemin du répertoire à lister. + * @return Liste des noms de fichiers et répertoires. + * @throws IllegalArgumentException Si le répertoire n'existe pas. + */ + public List ls(String dirPath) { + if (!this.directories.containsKey(dirPath)) + { throw new IllegalArgumentException("Répertoire introuvable: " + dirPath); } + + List contents = new ArrayList<>(); + Set dirContents = this.directories.get(dirPath); + + for (String item : dirContents) { + String fullPath = dirPath.equals(ROOT) ? ROOT + item : dirPath + "/" + item; + if (this.directories.containsKey(fullPath)) { + contents.add(item + "/"); + } else if (this.files.containsKey(fullPath)) { + contents.add(item); + } + } + + Collections.sort(contents); + return contents; + } + + /** + * Crée un nouveau fichier vide. + * @param fileName Nom du fichier à créer. + * @return true si le fichier a été créé, false s'il existe déjà. + * @throws IllegalArgumentException Si fileName est vide ou null. + */ + public boolean touch(String fileName) { + if (fileName == null || fileName.trim().isEmpty()) + { throw new IllegalArgumentException("Nom de fichier invalide."); } + + String fullPath = this.getFullPath(fileName); + + if (this.files.containsKey(fullPath)) { return false; } + + this.files.put(fullPath, ""); + this.directories.get(this.currentDirectory).add(fileName); + return true; + } + + /** + * Lit le contenu d'un fichier. + * @param fileName Nom du fichier à lire. + * @return Contenu du fichier, ou un message d'erreur si le fichier n'existe pas. + */ + public String cat(String fileName) { + String fullPath = this.getFullPath(fileName); + return this.files.getOrDefault(fullPath, "Fichier introuvable: " + fileName); + } + + /** + * Écrit du contenu dans un fichier (écrasement). + * @param fileName Nom du fichier. + * @param content Contenu à écrire. + * @return true si l'écriture a réussi, false sinon. + */ + public boolean echo(String fileName, String content) { + if (fileName == null || fileName.trim().isEmpty()) { return false; } + + String fullPath = this.getFullPath(fileName); + this.files.put(fullPath, content); + return true; + } + + /** + * Ajoute du contenu à la fin d'un fichier. + * @param fileName Nom du fichier. + * @param content Contenu à ajouter. + * @return true si l'ajout a réussi, false sinon. + */ + public boolean append(String fileName, String content) { + if (fileName == null || fileName.trim().isEmpty()) { return false; } + + String fullPath = getFullPath(fileName); + String existingContent = this.files.getOrDefault(fullPath, ""); + this.files.put(fullPath, existingContent + content); + return true; + } + + /** + * Construit le chemin absolu à partir d'un nom de fichier/répertoire. + * @param name Nom du fichier ou répertoire. + * @return Chemin absolu. + */ + private String getFullPath(String name) { + if (this.currentDirectory.equals(FileSystem.ROOT)) + { return FileSystem.ROOT + name; } + return this.currentDirectory + "/" + name; + } + + /** + * Vérifie si un fichier ou répertoire existe. + * @param name Nom du fichier ou répertoire. + * @return true s'il existe, false sinon. + */ + public boolean exists(String name) { + String fullPath = getFullPath(name); + return this.files.containsKey(fullPath) || this.directories.containsKey(fullPath); + } + + /** + * Retourne le chemin absolu du répertoire parent. + * @param path Chemin dont on veut le parent. + * @return Chemin absolu du répertoire parent. + */ + public String getParentDirectory(String path) { + if (path.equals(FileSystem.ROOT)) { return FileSystem.ROOT; } + + String[] parts = path.split("/"); + if (parts.length <= 2) { return FileSystem.ROOT; } + + StringBuilder parentPath = new StringBuilder(); + for (int i = 1; i < parts.length - 1; i++) + { parentPath.append("/").append(parts[i]); } + + return parentPath.length() == 0 ? FileSystem.ROOT : parentPath.toString(); + } + + /** + * Résout un chemin (relatif ou absolu) en chemin absolu. + * @param path Chemin à résoudre (peut être relatif ou absolu). + * @return Chemin absolu résolu. + */ + public String resolvePath(String path) { + if (path == null || path.trim().isEmpty()) + { return this.currentDirectory; } + + path = path.trim(); + + if (path.startsWith(ROOT)) + { return this.normalizePath(path); } + + String resolvedPath = this.currentDirectory + "/" + path; + return normalizePath(resolvedPath); + } + + /** + * Normalise un chemin (supprime les "/./" et résout les "/../"). + * @param path Chemin à normaliser. + * @return Chemin normalisé. + */ + public String normalizePath(String path) { + if (path.equals(FileSystem.ROOT)) { return FileSystem.ROOT; } + + String[] parts = path.split("/"); + List normalizedParts = new ArrayList<>(); + + for (String part : parts) { + if (part.isEmpty() || part.equals(".")) + { continue; } + else if (part.equals("..")) { + if (!normalizedParts.isEmpty()) { + normalizedParts.remove(normalizedParts.size() - 1); + } + } + else { normalizedParts.add(part); } + } + + if (normalizedParts.isEmpty()) + { return FileSystem.ROOT; } + + StringBuilder normalizedPath = new StringBuilder(); + for (String part : normalizedParts) + { normalizedPath.append("/").append(part); } + + return normalizedPath.toString(); + } + + /** + * Vérifie si un chemin est un répertoire. + * @param path Chemin à vérifier. + * @return true si c'est un répertoire, false sinon. + */ + public boolean isDirectory(String path) + { return this.directories.containsKey(path); } + + /** + * Vérifie si un chemin est un fichier. + * @param path Chemin à vérifier. + * @return true si c'est un fichier, false sinon. + */ + public boolean isFile(String path) + { return this.files.containsKey(path); } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/MainTerminal2.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/MainTerminal2.java new file mode 100644 index 0000000..4c71466 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/MainTerminal2.java @@ -0,0 +1,10 @@ +package gabywald.terminal2; + +/** + * Classe principale pour lancer l'émulateur de terminal. + * @author Gabriel Chandesris (2026) + * @deprecated Use {@code gabywald.launchers.TerminalEmulatorLaunchers} + */ +public class MainTerminal2 { + public static void main(String[] args) { new TerminalEmulator(); } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/Pipe.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/Pipe.java new file mode 100644 index 0000000..19edac7 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/Pipe.java @@ -0,0 +1,39 @@ +package gabywald.terminal2; + +import java.util.ArrayList; +import java.util.List; + +/** + * Classe pour gérer les pipes entre commandes. + * @author Gabriel Chandesris (2026) + */ +public class Pipe { + private List commands; + + /** + * Constructeur. + * @param commands Liste des commandes séparées par des pipes. + */ + public Pipe(List commands) { this.commands = commands; } + + /** + * Retourne la liste des commandes. + */ + public List getCommands() { return this.commands; } + + /** + * Analyse une chaîne de commandes avec pipes. + * @param input Chaîne complète (ex: "ls | grep txt"). + * @return Objet Pipe contenant les commandes séparées. + */ + public static Pipe parsePipe(String input) { + String[] parts = input.split("\\|"); + List commandList = new ArrayList<>(); + for (String part : parts) { + String trimmedPart = part.trim(); + if (!trimmedPart.isEmpty()) + { commandList.add(trimmedPart.split("\\s+")); } + } + return new Pipe(commandList); + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/Redirection.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/Redirection.java new file mode 100644 index 0000000..42ca9b4 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/Redirection.java @@ -0,0 +1,86 @@ +package gabywald.terminal2; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Classe pour gérer les redirections d'entrée/sortie (>, >>, <). + * @author Gabriel Chandesris (2026) + */ +public class Redirection { + /** + * Type de redirection. + */ + public enum Type { + NONE, // Pas de redirection + OUTPUT, // > (écrasement) + APPEND, // >> (ajout) + INPUT // < (entrée) + } + + private Type type; + private String fileName; + public String[] cleanedArgs; // Arguments nettoyés (sans les redirections) + + /** + * Constructeur. + * @param type Type de redirection. + * @param fileName Nom du fichier. + */ + public Redirection(Type type, String fileName) { + this.type = type; + this.fileName = fileName; + } + + /** + * Retourne le type de redirection. + */ + public Type getType() { return this.type; } + + /** + * Retourne le nom du fichier. + */ + public String getFileName() { return this.fileName; } + + /** + * Analyse une commande pour extraire les redirections. + * @param args Arguments de la commande. + * @return Liste des redirections trouvées. + */ + public static List parseRedirections(String[] args) { + List redirections = new ArrayList<>(); + List cleanedArgs = new ArrayList<>(); + + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if (arg.equals(">") && i + 1 < args.length) { + redirections.add(new Redirection(Type.OUTPUT, args[i + 1])); + i++; + } else if (arg.equals(">>") && i + 1 < args.length) { + redirections.add(new Redirection(Type.APPEND, args[i + 1])); + i++; + } else if (arg.equals("<") && i + 1 < args.length) { + redirections.add(new Redirection(Type.INPUT, args[i + 1])); + i++; + } else { cleanedArgs.add(arg); } + } + + if (!cleanedArgs.isEmpty() && !redirections.isEmpty()) { + redirections.get(0).cleanedArgs = cleanedArgs.toArray(new String[0]); + } + + return redirections; + } + + @Override + public String toString() { + StringBuilder sbToReturn = new StringBuilder(); + + sbToReturn.append(this.fileName).append(this.type.name()).append("\n"); + Arrays.asList(this.cleanedArgs).stream().forEach(elt -> sbToReturn.append("\t").append(elt).append("\n")); + + return sbToReturn.toString(); + } + +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/TerminalEmulator.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/TerminalEmulator.java new file mode 100644 index 0000000..5cdae40 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/TerminalEmulator.java @@ -0,0 +1,222 @@ +package gabywald.terminal2; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; + +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.JTextPane; +import javax.swing.text.BadLocationException; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyleContext; +import javax.swing.text.StyledDocument; + +import gabywald.terminal2.commands.CommandParser; +import gabywald.terminal2.editors.NanoEditorSwing; +import gabywald.terminal2.editors.TextEditorSwing; +import gabywald.terminal2.editors.VimEditorSwing; + +/** + * Classe principale pour l'interface graphique de l'émulateur de terminal. + * Gère l'affichage, la saisie des commandes, et les modes d'édition. + * @author Gabriel Chandesris (2026) + */ +public class TerminalEmulator { + private JFrame frame; + private JTextPane outputArea; + private JTextField inputField; + private FileSystem fileSystem; + private CommandParser commandParser; + private String currentDirectory; + + // For Edition Mode + private TextEditorSwing currentEditor; + private String currentEditFile; + private boolean isEditing = false; + + /** + * Constructor : initialize graphical Interface and File System + */ + public TerminalEmulator() { + this.fileSystem = new FileSystem(); + this.commandParser = new CommandParser(this.fileSystem); + this.currentDirectory = this.fileSystem.getRoot(); + this.initializeUI(); + } + + /** + * Initialise User Interface (UI). + */ + private void initializeUI() { + this.frame = new JFrame("Terminal Emulator"); + this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + this.frame.setSize(800, 600); + this.frame.setLayout(new BorderLayout()); + + // Not editable output Zone + this.outputArea = new JTextPane(); + this.outputArea.setEditable(false); + this.outputArea.setBackground(Color.BLACK); + this.outputArea.setForeground(Color.GREEN); + this.outputArea.setFont(new Font("Monospaced", Font.PLAIN, 14)); + JScrollPane scrollPane = new JScrollPane(this.outputArea); + this.frame.add(scrollPane, BorderLayout.CENTER); + + // Input Field + this.inputField = new JTextField(); + this.inputField.setBackground(Color.BLACK); + this.inputField.setForeground(Color.GREEN); + this.inputField.setFont(new Font("Monospaced", Font.PLAIN, 14)); + this.inputField.addActionListener(e -> executeCommand()); + this.frame.add(this.inputField, BorderLayout.SOUTH); + + // Show Welcome Message + this.printWelcomeMessage(); + // Show Initial Prompt + this.printPrompt(); + + this.frame.setVisible(true); + } + + private void printWelcomeMessage() { + this.appendToOutput("===============================================\n"); + this.appendToOutput(" TERMINAL EMULATOR (V2) - Java 8 / Swing\n"); + this.appendToOutput(" Type 'help' for a list of available commands\n"); + // this.appendToOutput(" Type 'exit' to quit\n"); + this.appendToOutput("===============================================\n\n"); + } + + /** + * Show Prompt in Output Zone + */ + private void printPrompt() { + try { + StyledDocument doc = this.outputArea.getStyledDocument(); + doc.insertString(doc.getLength(), currentDirectory + " > ", getPromptStyle()); + } catch (BadLocationException ex) { + ex.printStackTrace(); + } + } + + /** + * Return Prompt Stule + */ + private Style getPromptStyle() { + StyleContext sc = StyleContext.getDefaultStyleContext(); + Style style = sc.addStyle("PromptStyle", null); + StyleConstants.setForeground(style, Color.GREEN); + StyleConstants.setBold(style, true); + return style; + } + + /** + * Execute command given by User + */ + private void executeCommand() { + String command = this.inputField.getText().trim(); + this.inputField.setText(""); + + if (command.isEmpty()) { + this.printPrompt(); + return; + } + + this.appendToOutput(command + "\n"); + + if (this.isEditing) { + this.handleEditorInput(command); + return; + } + + if (command.startsWith("edit ")) { + this.startEditMode(command.substring(5).trim()); + return; + } + + String output = this.commandParser.execute(command, this.currentDirectory); + if (output.startsWith("MODE_EDIT:")) + { this.startEditMode(output.substring(10)); } + else + { this.appendToOutput(output + "\n"); } + + if (command.startsWith("cd ")) + { this.currentDirectory = this.fileSystem.getCurrentDirectory(); } + + this.printPrompt(); + } + + /** + * Start Editing mode (nano or vim) + */ + private void startEditMode(String args) { + this.isEditing = true; + String[] editArgs = args.split("\\s+"); + String editorType = "nano"; + String fileName; + + if (editArgs.length > 0 && (editArgs[0].equals("--nano") || editArgs[0].equals("--vim"))) { + editorType = editArgs[0].substring(2); + if (editArgs.length < 2) { + appendToOutput("Usage: edit [--nano|--vim] \n"); + this.isEditing = false; + printPrompt(); + return; + } + fileName = editArgs[1]; + } else { fileName = editArgs[0]; } + + this.currentEditFile = fileName; + + if (editorType.equals("nano")) + { this.currentEditor = new NanoEditorSwing(this, this.fileSystem, fileName); } + else + { this.currentEditor = new VimEditorSwing(this, this.fileSystem, fileName); } + + this.appendToOutput("--- Edition with '" + editorType + "' ---\n"); + this.currentEditor.start(); + } + + /** + * Gère les entrées en mode édition. + */ + private void handleEditorInput(String input) { + String result = this.currentEditor.handleInput(input); + if (result != null) { + if (result.startsWith("SAVE:")) { + this.fileSystem.echo(this.currentEditFile, result.substring(5)); + this.appendToOutput("File '" + this.currentEditFile + "' recorded.\n"); + } else if (result.equals("CANCEL")) { + this.appendToOutput("Edition Canceled.\n"); + } + this.endEditMode(); + } else { + appendToOutput(input + "\n"); + } + } + + /** + * End Edtion Mode + */ + private void endEditMode() { + this.isEditing = false; + this.currentEditor = null; + this.currentEditFile = null; + this.printPrompt(); + } + + /** + * Add Text to Output Zone + */ + public void appendToOutput(String text) { + try { + StyledDocument doc = this.outputArea.getStyledDocument(); + doc.insertString(doc.getLength(), text, null); + this.outputArea.setCaretPosition(doc.getLength()); + } catch (BadLocationException ex) { + ex.printStackTrace(); + } + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/CatCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/CatCommand.java new file mode 100644 index 0000000..fc2741a --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/CatCommand.java @@ -0,0 +1,31 @@ +package gabywald.terminal2.commands; + +import gabywald.terminal2.FileSystem; + +/** + * Commande 'cat' : affiche le contenu d'un fichier ou de stdin. + * @author Gabriel Chandesris (2026) + */ +public class CatCommand implements Command { + private FileSystem fileSystem; + + public CatCommand(FileSystem fileSystem) { this.fileSystem = fileSystem; } + + @Override + public String execute(String[] args, String stdin) { + if (args.length == 0) { + if (stdin != null) { return stdin; } + return "Usage: cat ou utiliser une redirection (<)"; + } + + StringBuilder output = new StringBuilder(); + for (String fileName : args) { + if (this.fileSystem.exists(fileName)) { + output.append(this.fileSystem.cat(fileName)); + } else { + output.append("Fichier introuvable: ").append(fileName); + } + } + return output.toString(); + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/CdCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/CdCommand.java new file mode 100644 index 0000000..05498e6 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/CdCommand.java @@ -0,0 +1,38 @@ +package gabywald.terminal2.commands; + +import gabywald.terminal2.FileSystem; + +/** + * Commande 'cd' : change le répertoire courant. + * @author Gabriel Chandesris (2026) + */ +public class CdCommand implements Command { + private FileSystem fileSystem; + + public CdCommand(FileSystem fileSystem) { + this.fileSystem = fileSystem; + } + + @Override + public String execute(String[] args, String stdin) { + if (args.length == 0) { return "Usage: cd "; } + + String targetDir = args[0]; + if (targetDir.equals("..")) { + String parentDir = fileSystem.getParentDirectory(fileSystem.getCurrentDirectory()); + this.fileSystem.setCurrentDirectory(parentDir); + return ""; + } else if (targetDir.equals("/")) { + fileSystem.setCurrentDirectory(fileSystem.getRoot()); + return ""; + } else { + String currentDir = fileSystem.getCurrentDirectory(); + String fullPath = currentDir.equals("/") ? "/" + targetDir : currentDir + "/" + targetDir; + if (fileSystem.exists(targetDir)) { + fileSystem.setCurrentDirectory(fullPath); + return ""; + } + return "Répertoire introuvable: " + targetDir; + } + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/ClearCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/ClearCommand.java new file mode 100644 index 0000000..ef0f8eb --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/ClearCommand.java @@ -0,0 +1,11 @@ +package gabywald.terminal2.commands; + +/** + * Commande 'clear' : efface l'écran (simulé par des sauts de ligne). + * @author Gabriel Chandesris (2026) + */ +public class ClearCommand implements Command { + @Override + public String execute(String[] args, String stdin) + { return "\n\n\n\n\n\n\n\n\n\n"; } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/Command.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/Command.java new file mode 100644 index 0000000..6693844 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/Command.java @@ -0,0 +1,23 @@ +package gabywald.terminal2.commands; + +/** + * Interface pour les commandes du terminal. + * Supporte les entrées standard (stdin) pour les pipes et redirections. + * @author Gabriel Chandesris (2026) + */ +public interface Command { + /** + * Exécute la commande avec les arguments fournis. + * @param args Arguments de la commande. + * @return Résultat de l'exécution. + */ + default String execute(String[] args) { return this.execute(CommandParser.removefirstFrom(args), null); } + + /** + * Exécute la commande avec une entrée standard (pour les pipes et redirections <). + * @param args Arguments de la commande. + * @param stdin Entrée standard (peut être null). + * @return Résultat de l'exécution. + */ + String execute(String[] args, String stdin); +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/CommandParser.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/CommandParser.java new file mode 100644 index 0000000..9852694 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/CommandParser.java @@ -0,0 +1,154 @@ +package gabywald.terminal2.commands; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import gabywald.terminal2.FileSystem; +import gabywald.terminal2.Pipe; +import gabywald.terminal2.Redirection; + +/** + * Classe responsable de l'analyse et de l'exécution des commandes. + * Supporte les redirections (>, >>, <) et les pipes (|). + * @author Gabriel Chandesris (2026) + */ +public class CommandParser { + private FileSystem fileSystem; + private Map commands; + + /** + * Constructeur : initialise les commandes disponibles. + */ + public CommandParser(FileSystem fileSystem) { + this.fileSystem = fileSystem; + this.commands = new HashMap<>(); + this.initializeCommands(); + } + + /** + * Initialise les commandes disponibles. + */ + private void initializeCommands() { + this.commands.put("ls", new LsCommand(fileSystem)); + this.commands.put("cd", new CdCommand(fileSystem)); + this.commands.put("cat", new CatCommand(fileSystem)); + this.commands.put("echo", new EchoCommand(fileSystem)); + this.commands.put("mkdir", new MkdirCommand(fileSystem)); + this.commands.put("touch", new TouchCommand(fileSystem)); + this.commands.put("rm", new RmCommand(fileSystem)); + this.commands.put("pwd", new PwdCommand(fileSystem)); + this.commands.put("help", new HelpCommand()); + this.commands.put("clear", new ClearCommand()); + this.commands.put("edit", new EditCommand(fileSystem, this)); + this.commands.put("run", new RunCommand(fileSystem, this)); + this.commands.put("grep", new GrepCommand(fileSystem)); + this.commands.put("wc", new WcCommand(fileSystem)); + this.commands.put("tr", new TrCommand(fileSystem)); + this.commands.put("sort", new SortCommand(fileSystem)); + } + + /** + * Exécute une commande avec gestion des redirections et pipes. + * @param input Commande complète (ex: "ls | grep txt"). + * @param currentDirectory Répertoire courant. + * @return Résultat de l'exécution. + */ + public String execute(String input, String currentDirectory) { + input = input.trim(); + if (input.isEmpty()) { return ""; } + + if (input.contains("|")) { return this.executePipe(input); } + + String[] args = input.split("\\s+"); + List redirections = Redirection.parseRedirections(args); + + if (redirections.isEmpty()) + { return this.executeCommand(args, currentDirectory, null); } + else + { return this.executeWithRedirections(redirections, currentDirectory); } + } + + /** + * Execute command with Redirections + */ + private String executeWithRedirections(List redirections, String currentDirectory) { + Redirection firstRedirection = redirections.get(0); + String[] args = firstRedirection.cleanedArgs; + + if (args.length == 0) { return "Commande invalide."; } + + String commandName = args[0]; + Command command = this.commands.get(commandName); + if (command == null) { return "Commande introuvable: " + commandName; } + + String stdin = null; + for (Redirection r : redirections) { + if (r.getType() == Redirection.Type.INPUT) { + if (this.fileSystem.exists(r.getFileName())) + { stdin = this.fileSystem.cat(r.getFileName()); } + else + { return "Fichier introuvable pour l'entrée: " + r.getFileName(); } + } + } + + String output = command.execute(CommandParser.removefirstFrom(args), stdin); + + for (Redirection r : redirections) { + if (r.getType() == Redirection.Type.OUTPUT || r.getType() == Redirection.Type.APPEND) { + if (r.getType() == Redirection.Type.OUTPUT) + { this.fileSystem.echo(r.getFileName(), output); } + else { // Redirection.Type.APPEND + String existingContent = this.fileSystem.exists(r.getFileName()) ? + this.fileSystem.cat(r.getFileName()) : ""; + this.fileSystem.echo(r.getFileName(), existingContent + output); + } + return ""; + } + } + + return output; + } + + /** + * Execute Command without Redirection + */ + private String executeCommand(String[] args, String currentDirectory, String stdin) { + if (args.length == 0) { return ""; } + + String commandName = args[0]; + Command command = this.commands.get(commandName); + if (command == null) { return "Commande introuvable: " + commandName; } + + return command.execute(CommandParser.removefirstFrom(args), stdin); + } + + /** + * Remove first elt of agrs ('command'). + */ + static String[] removefirstFrom(String[] args) + { return Arrays.asList(args).subList(1, args.length).toArray(new String[args.length - 1]); } + + /** + * Execute chain of Commands with pipes + */ + private String executePipe(String input) { + Pipe pipe = Pipe.parsePipe(input); + List commandList = pipe.getCommands(); + + if (commandList.isEmpty()) { return ""; } + + String currentOutput = this.executeCommand(commandList.get(0), this.fileSystem.getCurrentDirectory(), null); + + for (int i = 1; i < commandList.size(); i++) { + String[] args = commandList.get(i); + String commandName = args[0]; + Command command = this.commands.get(commandName); + if (command == null) { return "Commande introuvable dans le pipe: " + commandName; } + currentOutput = command.execute(CommandParser.removefirstFrom(args), currentOutput); + } + + return currentOutput; + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/EchoCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/EchoCommand.java new file mode 100644 index 0000000..6f7dac0 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/EchoCommand.java @@ -0,0 +1,22 @@ +package gabywald.terminal2.commands; + +import gabywald.terminal2.FileSystem; + +/** + * Commande 'echo' : affiche du texte. + * @author Gabriel Chandesris (2026) + */ +public class EchoCommand implements Command { + private FileSystem fileSystem; + + public EchoCommand(FileSystem fileSystem) { this.fileSystem = fileSystem; } + + @Override + public String execute(String[] args, String stdin) { + if (args.length == 0) { return "Usage: echo "; } + + StringBuilder text = new StringBuilder(); + for (String arg : args) { text.append(arg).append(" "); } // .replaceAll("\"", "") + return text.toString().trim(); + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/EditCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/EditCommand.java new file mode 100644 index 0000000..5c3f3d5 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/EditCommand.java @@ -0,0 +1,27 @@ +package gabywald.terminal2.commands; + +import gabywald.terminal2.FileSystem; + +/** + * Commande 'edit' : lance un éditeur de texte (nano ou vim). + * Usage: + * edit # Utilise nano par défaut + * edit --nano # Force nano + * edit --vim # Force vim + * @author Gabriel Chandesris (2026) + */ +public class EditCommand implements Command { + private FileSystem fileSystem; + private CommandParser commandParser; + + public EditCommand(FileSystem fileSystem, CommandParser commandParser) { + this.fileSystem = fileSystem; + this.commandParser = commandParser; + } + + @Override + public String execute(String[] args, String stdin) { + if (args.length == 0) { return "Usage: edit [--nano|--vim] "; } + return "MODE_EDIT:" + String.join(" ", args); + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/GrepCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/GrepCommand.java new file mode 100644 index 0000000..ba92f30 --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/GrepCommand.java @@ -0,0 +1,40 @@ +package gabywald.terminal2.commands; + +import gabywald.terminal2.FileSystem; + +/** + * Commande 'grep' : filtre les lignes contenant un motif. + * Usage: grep [fichier] ou grep < stdin + * @author Gabriel Chandesris (2026) + */ +public class GrepCommand implements Command { + private FileSystem fileSystem; + + public GrepCommand(FileSystem fileSystem) { this.fileSystem = fileSystem; } + + @Override + public String execute(String[] args, String stdin) { + if (args.length == 0) { return "Usage: grep [file]"; } + + String pattern = args[0]; + String content; + + if (args.length > 1) { + String fileName = args[1]; + if (!this.fileSystem.exists(fileName)) + { return "Fichier introuvable: " + fileName; } + content = this.fileSystem.cat(fileName); + } else if (stdin != null) { + content = stdin; + } else { + return "Usage: grep [file] or use pipe"; + } + + StringBuilder output = new StringBuilder(); + for (String line : content.split("\n")) { + if (line.contains(pattern)) { output.append(line).append("\n"); } + } + + return output.toString(); + } +} diff --git a/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/HelpCommand.java b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/HelpCommand.java new file mode 100644 index 0000000..17fbbdc --- /dev/null +++ b/testideas-terminalEmulator/src/main/java/gabywald/terminal2/commands/HelpCommand.java @@ -0,0 +1,36 @@ +package gabywald.terminal2.commands; + +/** + * Command 'help' : Show Availaible Command List. + * @author Gabriel Chandesris (2026) + */ +public class HelpCommand implements Command { + @Override + public String execute(String[] args, String stdin) { + return "Commandes disponibles:\n" + + " ls - Lister les fichiers/répertoires\n" + + " cd - Changer de répertoire\n" + + " cat - Afficher le contenu d'un fichier\n" + + " echo - Afficher du texte\n" + + " echo > - Écrire dans un fichier (écrasement)\n" + + " echo >> - Ajouter à un fichier\n" + + " mkdir - Créer un répertoire\n" + + " touch - Créer un fichier\n" + + " rm - Supprimer un fichier/répertoire\n" + + " pwd - Afficher le répertoire courant\n" + + " edit - Éditer un fichier (nano par défaut)\n" + + " edit --nano - Éditer avec nano\n" + + " edit --vim - Éditer avec vim\n" + + " run