From: Svjatoslav Agejenko Date: Sun, 27 Sep 2026 15:57:17 +0000 (+0300) Subject: initial commit X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=24b9f92f07193878ca29f092be41e69eb62a9575;p=instantlauncher.git initial commit --- 24b9f92f07193878ca29f092be41e69eb62a9575 diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..46bbcca --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.classpath +/target/ +/bin +/.settings +/.idea/ +/*.iml +*.html diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/Documentation/index.org b/Documentation/index.org new file mode 100644 index 0000000..141ef0d --- /dev/null +++ b/Documentation/index.org @@ -0,0 +1,180 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Instantlauncher - navigate to your files and programs quickly! +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +* Introduction + +Quick filesystem based document/application launcher. It starts by +showing content of [[id:5ced6bf5-165a-49a0-8de4-3755d810d987][predefined directory]]. Sub directories can be chosen +by simply moving mouse pointer over corresponding directory. + +[[file:screenshot.png]] + +Files types are color-coded. + +** File type colors +:PROPERTIES: +:ID: ab6a1982-b856-403c-9960-43680720399a +:END: + +Colors come from the file type (selected entry = brighter shade): + ++ blue :: Directory - including a symlink that points to a directory ++ red :: Executable file ++ green :: Non-executable file ++ olive/yellow :: Anything that is neither directory nor regular file, + e.g. a broken symlink + +Note: a symlink pointing to a file takes the *file* colors +(red/green), not blue. The Usage table's "Directory/symlink (blue)" +holds only for symlinks pointing to directories. + +** Usage +Simply hovering mouse pointer over directory expands its contents in +the next right column. + + ++ Executable file (red): + + Left mouse click: + + Run executable file. + + Right mouse click: + + Toggle executable state. + ++ Non-executable file (green): + + Left mouse click: + + Open file using corresponding application. + + Right mouse click: + + Toggle executable state. + ++ Directory/symlink (blue): + + Left mouse click: + + Open directory using configured filesystem browser. + + Right mouse click: + + Open directory using configured text based terminal emulator. + +* Installation +:PROPERTIES: +:ID: aea98e61-17aa-4c69-acd3-a11e043088a6 +:END: + +Run the install script from the project root (asks for sudo): +#+BEGIN_SRC sh +./install +#+END_SRC + +The script does everything: builds the jar (=mvn clean package=, +requires JDK 21 and Maven), copies =instantlauncher.jar= and the +=instantlauncher= launcher script to =/opt/instantlauncher/=, and +symlinks the launcher into =/usr/bin/=. + +After that the program starts by typing =instantlauncher= in a terminal. +Only a Java runtime is needed at launch; the launcher script runs +=java -jar /opt/instantlauncher/instantlauncher.jar=. + +The jar name carries no version number, so these scripts never change +when the project version does. Re-running =install= is safe: it +rebuilds and updates the installed copy. + +** Configuration +When instantlauncher starts, it looks for configuration file under +: ~/.instantlauncher + +If file is not found, it creates new default configuration file in +YAML format: +#+BEGIN_SRC +!configuration +directoryOpenCommand: nautilus -w {file} +directoryTerminalOpenCommand: gnome-terminal --working-directory={file} +fileAssociations: +- command: vlc {file} + fileRegex: .*\.mkv$ +- command: emacs {file} + fileRegex: .*\.txt$ +- command: eog {file} + fileRegex: .*\.jpeg$ +- command: eog {file} + fileRegex: .*\.png$ +- command: vlc {file} + fileRegex: .*\.avi$ +- command: vlc {file} + fileRegex: .*\.mp4$ +- command: eog {file} + fileRegex: .*\.jpg$ +- command: emacs {file} + fileRegex: .*\.org$ +navigationRootPath: /home/svjatoslav/ +#+END_SRC + +*** navigationRootPath + :PROPERTIES: + :ID: 5ced6bf5-165a-49a0-8de4-3755d810d987 + :END: +Root directory from where to start navigation. + +This parameter is user specific! In my case instantlauncher by default +pointed this to my home directory. But it can be any directory user +chooses. + +I think good use-case is to create special directory to be used for +instantlauncher root. This special root directory can in turn contain +symlinks to various places user wants to visit often. Instantlauncher +will follow symlinks as if they were directories. + +: navigationRootPath: /home/svjatoslav/ + +*** fileAssociations +Allows to map file name/extension using regular expression pattern +matching to corresponding application. + +Example: +#+BEGIN_SRC conf +- command: vlc {file} + fileRegex: .*\.mkv$ +#+END_SRC + +Where: +- command :: Command to execute to open given file. Special variable + {file} will point to user chosen file using absolute file + path. + +- fileRegex :: Regular expression that must match the file. + +*** directoryOpenCommand +Command to use when opening directory. + +By default instantlauncher assumes that Gnome desktop environment is +installed and uses Nautilus file manager: +: directoryOpenCommand: nautilus -w {file} + +*** directoryTerminalOpenCommand +Command to use when opening directory in text terminal based console. + +By default instantlauncher assumes that Gnome desktop environment is +installed and uses Gnome Terminal: + +: directoryTerminalOpenCommand: gnome-terminal --working-directory={file} +* Source code +- This program is free software: released under Creative Commons Zero + (CC0) license + +- Program author: + - Svjatoslav Agejenko + - Homepage: https://svjatoslav.eu + - Email: mailto://svjatoslav@svjatoslav.eu + +- [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]] + +** Source code +- [[https://www2.svjatoslav.eu/gitweb/?p=instantlauncher.git;a=snapshot;h=HEAD;sf=tgz][Download latest snapshot in TAR GZ format]] + +- [[https://www2.svjatoslav.eu/gitweb/?p=instantlauncher.git;a=summary][Browse Git repository online]] + +- Clone Git repository using command: + : git clone https://www2.svjatoslav.eu/git/instantlauncher.git + diff --git a/Documentation/screenshot.png b/Documentation/screenshot.png new file mode 100644 index 0000000..5b840af Binary files /dev/null and b/Documentation/screenshot.png differ diff --git a/Tools/Open with IntelliJ IDEA b/Tools/Open with IntelliJ IDEA new file mode 100755 index 0000000..de9bae5 --- /dev/null +++ b/Tools/Open with IntelliJ IDEA @@ -0,0 +1,18 @@ +#!/bin/bash + +# +# This is a helper bash script that starts IntelliJ with the current project. +# Script is written is such a way that you can simply click on it in file +# navigator to run it. +# +# +# Script assumes: +# +# + GNU operating system +# + IntelliJ is installed and commandline launcher "idea" is enabled. +# + +cd "${0%/*}" +cd .. + +setsid idea . &>/dev/null diff --git a/Tools/Update web site b/Tools/Update web site new file mode 100755 index 0000000..3d35617 --- /dev/null +++ b/Tools/Update web site @@ -0,0 +1,89 @@ +#!/bin/bash +cd "${0%/*}"; if [ "$1" != "T" ]; then gnome-terminal -e "'$0' T"; exit; fi; + +cd .. + +# Function to export org to html using emacs in batch mode +export_org_to_html() { + local org_file=$1 + local dir=$(dirname "$org_file") + local base=$(basename "$org_file" .org) + ( + cd "$dir" || return 1 + local html_file="${base}.html" + if [ -f "$html_file" ]; then + rm -f "$html_file" + fi + echo "Exporting: $org_file → $dir/$html_file" + emacs --batch -l ~/.emacs --visit="${base}.org" --funcall=org-html-export-to-html --kill + if [ $? -eq 0 ]; then + echo "✓ Successfully exported $org_file" + else + echo "✗ Failed to export $org_file" + return 1 + fi + ) +} + +export_org_files_to_html() { + echo "🔍 Searching for .org files in Documentation/ ..." + echo "=======================================" + + mapfile -t ORG_FILES < <(find Documentation -type f -name "*.org" | sort) + + if [ ${#ORG_FILES[@]} -eq 0 ]; then + echo "❌ No .org files found!" + return 1 + fi + + echo "Found ${#ORG_FILES[@]} .org file(s):" + printf '%s\n' "${ORG_FILES[@]}" + echo "=======================================" + + SUCCESS_COUNT=0 + FAILED_COUNT=0 + + for org_file in "${ORG_FILES[@]}"; do + export_org_to_html "$org_file" + if [ $? -eq 0 ]; then + ((SUCCESS_COUNT++)) + else + ((FAILED_COUNT++)) + fi + done + + echo "=======================================" + echo "📊 SUMMARY:" + echo " ✓ Successful: $SUCCESS_COUNT" + echo " ✗ Failed: $FAILED_COUNT" + echo " Total: $((SUCCESS_COUNT + FAILED_COUNT))" + echo "" + + return $FAILED_COUNT +} + +# Compile Emacs org-mode files into HTML format +export_org_files_to_html + +if [ $? -ne 0 ]; then + echo "✗ HTML export failed - NOT publishing." + echo "" + echo "Press ENTER to close this window." + read + exit 1 +fi + +# Upload assembled documentation to server +echo "📤 Uploading to server..." +rsync -avz --delete -e 'ssh -p 10006' Documentation/ \ + n0@www3.svjatoslav.eu:/mnt/big/projects/instantlauncher/ + +if [ $? -eq 0 ]; then + echo "✓ Upload completed successfully!" +else + echo "✗ Upload failed!" +fi + +echo "" +echo "Press ENTER to close this window." +read diff --git a/install b/install new file mode 100755 index 0000000..770fe25 --- /dev/null +++ b/install @@ -0,0 +1,11 @@ +#!/bin/bash + +# Requires: JDK 21 and Maven to build, sudo to install. +# Idempotent: safe to re-run, updates the installed copy. + +mvn clean package + +sudo mkdir -p /opt/instantlauncher +sudo cp target/instantlauncher.jar /opt/instantlauncher/ +sudo cp instantlauncher /opt/instantlauncher/ +sudo ln -sf /opt/instantlauncher/instantlauncher /usr/bin/ diff --git a/instantlauncher b/instantlauncher new file mode 100755 index 0000000..f56287b --- /dev/null +++ b/instantlauncher @@ -0,0 +1,5 @@ +#!/bin/bash + +set -f + +java -jar /opt/instantlauncher/instantlauncher.jar "$@" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f752dc9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,117 @@ + + 4.0.0 + eu.svjatoslav + instantlauncher + 1.0 + jar + Instant Launcher + popup panel with shortcuts for quick application launching + + + 21 + 21 + 21 + UTF-8 + UTF-8 + + + + + eu.svjatoslav + svjatoslavcommons + 1.9 + + + + com.esotericsoftware.yamlbeans + yamlbeans + 1.12 + + + + + instantlauncher + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + true + UTF-8 + + + + + maven-assembly-plugin + + + + eu.svjatoslav.instantlauncher.Main + + + + jar-with-dependencies + + + + + + + package-jar-with-dependencies + package + + single + + + false + + jar-with-dependencies + + + + eu.svjatoslav.instantlauncher.Main + + + + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.2 + + + org.apache.maven.scm + maven-scm-provider-gitexe + 1.9.4 + + + + + + + + + + + svjatoslav.eu + Svjatoslav repository + https://www3.svjatoslav.eu/maven/ + + + + + svjatoslav.eu + https://svjatoslav.eu + + + + \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/instantlauncher/InstantLauncher.java b/src/main/java/eu/svjatoslav/instantlauncher/InstantLauncher.java new file mode 100755 index 0000000..02fc7a4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/instantlauncher/InstantLauncher.java @@ -0,0 +1,53 @@ +/* + * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.instantlauncher; + +import eu.svjatoslav.instantlauncher.configuration.ConfigurationManager; +import eu.svjatoslav.instantlauncher.configuration.FileAssociation; + +import java.io.File; +import java.io.IOException; + +import static eu.svjatoslav.instantlauncher.Utils.runOpeningApplication; + +public class InstantLauncher { + + private final MainFrame mainFrame; + public ConfigurationManager configurationManager; + + public InstantLauncher() throws IOException { + configurationManager = new ConfigurationManager(); + mainFrame = new MainFrame(this); + } + + public static void exitProgram() { + System.exit(0); + } + + /** + * @return true if file was opened. false if + * unknown file type. + */ + public boolean openFile(final File file) { + FileAssociation fileAssociation = configurationManager.getConfiguration().findFileAssociation(file); + if (fileAssociation == null) return false; + + runOpeningApplication(fileAssociation.command, file); + return true; + } + + public void run() { + mainFrame.show(); + } + + public void openDirectory(File chosenFile) { + runOpeningApplication(configurationManager.getConfiguration().directoryOpenCommand, chosenFile); + } + + public void openDirectoryInTerminal(File chosenFile) { + runOpeningApplication(configurationManager.getConfiguration().directoryTerminalOpenCommand, chosenFile); + } + +} diff --git a/src/main/java/eu/svjatoslav/instantlauncher/Main.java b/src/main/java/eu/svjatoslav/instantlauncher/Main.java new file mode 100755 index 0000000..a849cc7 --- /dev/null +++ b/src/main/java/eu/svjatoslav/instantlauncher/Main.java @@ -0,0 +1,15 @@ +/* + * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.instantlauncher; + +import java.io.IOException; + +class Main { + + public static void main(final String[] args) throws IOException { + new InstantLauncher().run(); + } + +} diff --git a/src/main/java/eu/svjatoslav/instantlauncher/MainFrame.java b/src/main/java/eu/svjatoslav/instantlauncher/MainFrame.java new file mode 100755 index 0000000..3977104 --- /dev/null +++ b/src/main/java/eu/svjatoslav/instantlauncher/MainFrame.java @@ -0,0 +1,85 @@ +/* + * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.instantlauncher; + +import eu.svjatoslav.instantlauncher.menu.MultiLevelMenu; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; + +class MainFrame { + + private final InstantLauncher instantLauncher; + private JFrame frame; + private boolean isShowing; + + public MainFrame(final InstantLauncher instantLauncher) { + this.instantLauncher = instantLauncher; + } + + private void buildContentPane() { + + final MultiLevelMenu multiLevelMenu = new MultiLevelMenu(instantLauncher); + frame.getContentPane().add(multiLevelMenu.buildContentPanel(), BorderLayout.CENTER); + + } + + public void show() { + + if (!isShowing) { + // initialize frame + frame = new JFrame("InstantLauncher"); + + buildContentPane(); + + // registor window listener + final FrameWindowListener windowListener = new FrameWindowListener(); + frame.addWindowListener(windowListener); + + // maximize window + frame.setExtendedState(JFrame.MAXIMIZED_BOTH); + frame.setVisible(true); + + isShowing = true; + } + + } + + private static class FrameWindowListener implements WindowListener { + + @Override + public void windowActivated(final WindowEvent e) { + } + + @Override + public void windowClosed(final WindowEvent e) { + } + + @Override + public void windowClosing(final WindowEvent e) { + InstantLauncher.exitProgram(); + } + + @Override + public void windowDeactivated(final WindowEvent e) { + } + + @Override + public void windowDeiconified(final WindowEvent e) { + } + + @Override + public void windowIconified(final WindowEvent e) { + } + + @Override + public void windowOpened(final WindowEvent e) { + } + + } + +} diff --git a/src/main/java/eu/svjatoslav/instantlauncher/Utils.java b/src/main/java/eu/svjatoslav/instantlauncher/Utils.java new file mode 100755 index 0000000..7410bab --- /dev/null +++ b/src/main/java/eu/svjatoslav/instantlauncher/Utils.java @@ -0,0 +1,46 @@ +/* + * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.instantlauncher; + +import eu.svjatoslav.commons.gui.dialog.ExceptionDialog; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.IOException; + +public class Utils { + + private static final String FILE_INDICATOR = "{file}"; + + public static void setComponentSize(JComponent component, Dimension size) { + component.setMinimumSize(size); + component.setMaximumSize(size); + component.setSize(size); + component.setPreferredSize(size); + } + + public static void runOpeningApplication(String commands, final File file) { + try { + String[] commandsArray = commands.split("\\s+"); + for (int i=0; i< commandsArray.length; i++) + commandsArray[i] = commandsArray[i].replaceAll("\\{file\\}", file.getAbsolutePath()); + + Runtime.getRuntime().exec(commandsArray); + } catch (final IOException e) { + new ExceptionDialog(e); + } + } + + public static void executeCommand(String... c) { + try { + Runtime.getRuntime().exec(c); + } catch (final IOException e) { + new ExceptionDialog(e); + } + } + + +} diff --git a/src/main/java/eu/svjatoslav/instantlauncher/configuration/Configuration.java b/src/main/java/eu/svjatoslav/instantlauncher/configuration/Configuration.java new file mode 100644 index 0000000..7dac14c --- /dev/null +++ b/src/main/java/eu/svjatoslav/instantlauncher/configuration/Configuration.java @@ -0,0 +1,27 @@ +/* + * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.instantlauncher.configuration; + +import java.io.File; +import java.util.Set; + +public class Configuration { + public Set fileAssociations; + public String navigationRootPath; + public String directoryOpenCommand; + public String directoryTerminalOpenCommand; + + public FileAssociation findFileAssociation(File file) { + for (FileAssociation association : fileAssociations) + if (association.matchesFile(file)) return association; + + return null; + } + + public File getNavigationRootDirectory() { + return new File(navigationRootPath); + } + +} diff --git a/src/main/java/eu/svjatoslav/instantlauncher/configuration/ConfigurationManager.java b/src/main/java/eu/svjatoslav/instantlauncher/configuration/ConfigurationManager.java new file mode 100755 index 0000000..5a2e512 --- /dev/null +++ b/src/main/java/eu/svjatoslav/instantlauncher/configuration/ConfigurationManager.java @@ -0,0 +1,84 @@ +/* + * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.instantlauncher.configuration; + +import com.esotericsoftware.yamlbeans.YamlConfig; +import com.esotericsoftware.yamlbeans.YamlReader; +import com.esotericsoftware.yamlbeans.YamlWriter; + +import java.io.*; +import java.util.HashSet; + +public class ConfigurationManager { + + private static final String CONFIG_FILE_NAME = ".instantlauncher"; + private Configuration configuration; + + public ConfigurationManager() throws IOException { + initConfig(); + } + + public Configuration getConfiguration() { + return configuration; + } + + private File getPropertiesFile() { + return new File(System.getProperty("user.home") + "/" + CONFIG_FILE_NAME); + } + + + private void initConfig() throws IOException { + final File propertiesFile = getPropertiesFile(); + if (!propertiesFile.exists()) { + initDefaultConfiguration(); + } else + loadConfigFile(propertiesFile); + } + + private void loadConfigFile(File propertiesFile) throws IOException { + YamlReader reader = new YamlReader(new FileReader(propertiesFile)); + configureYaml(reader.getConfig()); + configuration = reader.read(Configuration.class); + if (configuration == null) + initDefaultConfiguration(); + } + + private void saveConfigFile() throws IOException { + YamlWriter writer = new YamlWriter(new FileWriter(getPropertiesFile())); + configureYaml(writer.getConfig()); + writer.write(configuration); + writer.close(); + } + + private void configureYaml(YamlConfig config) { + config.setClassTag("configuration", Configuration.class); + config.setPropertyElementType(Configuration.class, "fileAssociations", FileAssociation.class); + } + + private void registerFileAssociation(String fileRegex, String command) { + FileAssociation association = new FileAssociation(fileRegex, command); + configuration.fileAssociations.add(association); + } + + private void initDefaultConfiguration() throws IOException { + configuration = new Configuration(); + configuration.fileAssociations = new HashSet<>(); + + configuration.navigationRootPath = System.getProperty("user.home") + "/"; + configuration.directoryOpenCommand = "nautilus -w {file}"; + configuration.directoryTerminalOpenCommand = "gnome-terminal --working-directory={file}"; + + registerFileAssociation(".*\\.jpeg$", "eog {file}"); + registerFileAssociation(".*\\.jpg$", "eog {file}"); + registerFileAssociation(".*\\.png$", "eog {file}"); + registerFileAssociation(".*\\.txt$", "emacs {file}"); + registerFileAssociation(".*\\.org$", "emacs {file}"); + registerFileAssociation(".*\\.avi$", "vlc {file}"); + registerFileAssociation(".*\\.mp4$", "vlc {file}"); + registerFileAssociation(".*\\.mkv$", "vlc {file}"); + + saveConfigFile(); + } +} diff --git a/src/main/java/eu/svjatoslav/instantlauncher/configuration/FileAssociation.java b/src/main/java/eu/svjatoslav/instantlauncher/configuration/FileAssociation.java new file mode 100644 index 0000000..9f3a3be --- /dev/null +++ b/src/main/java/eu/svjatoslav/instantlauncher/configuration/FileAssociation.java @@ -0,0 +1,36 @@ +/* + * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.instantlauncher.configuration; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FileAssociation implements Comparable { + public String command; + public String fileRegex; + + public FileAssociation() { + } + + public FileAssociation(String fileRegex, String command) { + this.fileRegex = fileRegex; + this.command = command; + } + + @Override + public int compareTo(FileAssociation o) { + return fileRegex.compareTo(o.fileRegex); + } + + public boolean matchesFile(File file) { + String absolutePath = file.getAbsolutePath(); + + Pattern pattern = Pattern.compile(fileRegex); + Matcher matcher = pattern.matcher(absolutePath); + return matcher.matches(); + } + +} diff --git a/src/main/java/eu/svjatoslav/instantlauncher/menu/Menu.java b/src/main/java/eu/svjatoslav/instantlauncher/menu/Menu.java new file mode 100755 index 0000000..3c50d37 --- /dev/null +++ b/src/main/java/eu/svjatoslav/instantlauncher/menu/Menu.java @@ -0,0 +1,233 @@ +/* + * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.instantlauncher.menu; + +import eu.svjatoslav.commons.gui.dialog.ExceptionDialog; +import eu.svjatoslav.instantlauncher.Utils; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +class Menu { + + public static final Dimension SIZE_MENU_PANEL = new Dimension(210, 900); + private static final Color BUTTON_BG_DIRECTORY = new Color(0, 0, 60); + + private static final Color BUTTON_BG_DIRECTORY_SELECTED = new Color(0, 0, 100); + private static final Color BUTTON_BG_LINK = new Color(60, 60, 0); + + private static final Color BUTTON_BG_LINK_SELECTED = new Color(100, 100, 0); + private static final Color BUTTON_BG_EXECUTABLE_FILE = new Color(60, 0, 0); + + private static final Color BUTTON_BG_EXECUTABLE_FILE_SELECTED = new Color(100, 0, 0); + private static final Color BUTTON_BG_NONEXECUTABLE_FILE = new Color(0, 60, 0); + + private static final Color BUTTON_BG_NONEXECUTABLE_FILE_SELECTED = new Color(0, 100, 0); + private static final Dimension SIZE_BUTTON = new Dimension(200, 30); + private final Set menuListeners = new HashSet<>(); + private JPanel menuPanel; + private JButton selectedButton; + private File selectedFile; + + public Menu(final File directory) { + buildContentPanel(directory); + } + + /** + *
+     * Assign specific colors to:
+     *    Directory
+     *    Executable file
+     *    Non executable file
+     * 
+ */ + private static Color getFileColor(final File file, final boolean selected) { + if (file.isDirectory()) + if (selected) + return BUTTON_BG_DIRECTORY_SELECTED; + else + return BUTTON_BG_DIRECTORY; + + if (file.isFile()) + if (file.canExecute()) { + if (selected) + return BUTTON_BG_EXECUTABLE_FILE_SELECTED; + else + return BUTTON_BG_EXECUTABLE_FILE; + } else if (selected) + return BUTTON_BG_NONEXECUTABLE_FILE_SELECTED; + else + return BUTTON_BG_NONEXECUTABLE_FILE; + + if (selected) + return BUTTON_BG_LINK_SELECTED; + else + return BUTTON_BG_LINK; + } + + public void addMenuListener(final MenuListener menuListener) { + menuListeners.add(menuListener); + } + + private JButton buildButton(final File file) { + JButton button; + if (file.isDirectory()) + button = new JButton("[ " + file.getName() + " ]"); + else + button = new JButton(file.getName()); + + button.setBackground(getFileColor(file, false)); + button.setForeground(new Color(255, 255, 255)); + + button.setBorder(new EmptyBorder(5, 10, 5, 10)); + + final Menu.ButtonMouseListener mouseListener = new ButtonMouseListener(button, this, file); + button.addMouseListener(mouseListener); + + button.setHorizontalTextPosition(SwingConstants.CENTER); + + // set button size + Utils.setComponentSize(button, SIZE_BUTTON); + + return button; + } + + private void buildContentPanel(final File directory) { + menuPanel = new JPanel(); + + Utils.setComponentSize(menuPanel, SIZE_MENU_PANEL); + + final BoxLayout layout = new BoxLayout(menuPanel, BoxLayout.Y_AXIS); + menuPanel.setLayout(layout); + menuPanel.setBackground(Color.BLACK); + + final File[] listFiles = directory.listFiles(); + + // Sort files by name + Arrays.sort(listFiles, (f1, f2) -> { + + // directories come before files + if (f1.isFile() && f2.isDirectory()) + return 1; + if (f2.isFile() && f1.isDirectory()) + return -1; + + // order by file name + return f1.getName().toUpperCase().compareTo(f2.getName().toUpperCase()); + }); + + // add files to the panel + for (final File file : listFiles) + // ignore hidden files + if (!file.getName().startsWith(".")) { + menuPanel.add(Box.createVerticalStrut(10)); + menuPanel.add(buildButton(file)); + } + } + + public JPanel getMenuPanel() { + return menuPanel; + } + + private JButton getSelectedButton() { + return selectedButton; + } + + private File getSelectedFile() { + return selectedFile; + } + + private void menuItemHighlighted(final File chosenFile) { + for (final MenuListener menuListener : menuListeners) + menuListener.menuItemHighlighted(this, chosenFile); + } + + private void menuItemSelected(final File chosenFile) { + for (final MenuListener menuListener : menuListeners) + menuListener.menuItemSelected(chosenFile); + } + + private void menuItemSelectedAlternative(final File chosenFile) { + for (final MenuListener menuListener : menuListeners) + menuListener.menuItemSelectedAlternative(chosenFile); + } + + private void menuItemSelectedAlternative2(final File chosenFile) { + for (final MenuListener menuListener : menuListeners) + menuListener.menuItemSelectedAlternative2(chosenFile); + } + + private void setSelected(final JButton selectedButton, final File selectedFile) { + this.selectedButton = selectedButton; + this.selectedFile = selectedFile; + } + + public static class ButtonMouseListener implements MouseListener { + + final JButton parentButton; + final Menu parentMenu; + final File file; + + public ButtonMouseListener(final JButton button, final Menu parentMenu, final File file) { + parentButton = button; + this.parentMenu = parentMenu; + this.file = file; + } + + @Override + public void mouseClicked(final MouseEvent e) { + try { + + if (e.getButton() == 1) // left mouse click + parentMenu.menuItemSelected(file); + if (e.getButton() == 2) // middle mouse click + parentMenu.menuItemSelectedAlternative2(file); + if (e.getButton() == 3) //right mouse click + parentMenu.menuItemSelectedAlternative(file); + } catch (final Exception ex) { + new ExceptionDialog(ex); + } + } + + @Override + public void mouseEntered(final MouseEvent e) { + try { + if (parentButton != parentMenu.getSelectedButton()) { + + if (parentMenu.getSelectedButton() != null) + parentMenu.getSelectedButton().setBackground(getFileColor(parentMenu.getSelectedFile(), false)); + + parentMenu.setSelected(parentButton, file); + parentButton.setBackground(getFileColor(file, true)); + + parentMenu.menuItemHighlighted(file); + } + } catch (final Exception ex) { + new ExceptionDialog(ex); + } + } + + @Override + public void mouseExited(final MouseEvent e) { + } + + @Override + public void mousePressed(final MouseEvent e) { + } + + @Override + public void mouseReleased(final MouseEvent e) { + } + + } + +} diff --git a/src/main/java/eu/svjatoslav/instantlauncher/menu/MenuListener.java b/src/main/java/eu/svjatoslav/instantlauncher/menu/MenuListener.java new file mode 100755 index 0000000..d19317d --- /dev/null +++ b/src/main/java/eu/svjatoslav/instantlauncher/menu/MenuListener.java @@ -0,0 +1,31 @@ +/* + * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.instantlauncher.menu; + +import java.io.File; + +interface MenuListener { + + /** + * mouse over + */ + void menuItemHighlighted(Menu menu, java.io.File chosenFile); + + /** + * left click + */ + void menuItemSelected(File chosenFile); + + /** + * right click + */ + void menuItemSelectedAlternative(File chosenFile); + + /** + * middle click + */ + void menuItemSelectedAlternative2(File chosenFile); + +} diff --git a/src/main/java/eu/svjatoslav/instantlauncher/menu/MultiLevelMenu.java b/src/main/java/eu/svjatoslav/instantlauncher/menu/MultiLevelMenu.java new file mode 100755 index 0000000..d47b05c --- /dev/null +++ b/src/main/java/eu/svjatoslav/instantlauncher/menu/MultiLevelMenu.java @@ -0,0 +1,167 @@ +/* + * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.instantlauncher.menu; + +import eu.svjatoslav.instantlauncher.InstantLauncher; +import eu.svjatoslav.instantlauncher.Utils; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +import static eu.svjatoslav.instantlauncher.Utils.executeCommand; + +public class MultiLevelMenu implements MenuListener { + + private static final int VERTICAL_MENUS_COUNT = 7; + private static final Dimension CONTENT_PANEL_SIZE = new Dimension(1024, 900); + private final InstantLauncher instantLauncher; + private final ArrayList menus = new ArrayList<>(); + + private final ArrayList panels = new ArrayList<>(); + private JPanel contentPane; + + public MultiLevelMenu(final InstantLauncher instantLauncher) { + this.instantLauncher = instantLauncher; + } + + /** + * Adds new vertical menu. + * + * @return true if operation succeeded, or false + * if there was not enough space left for a new menu. + */ + private boolean addMenu(final File directory) { + final int newMenuHorizontalIndex = menus.size(); + + if (newMenuHorizontalIndex >= VERTICAL_MENUS_COUNT) { + return false; + } + + final Menu menu = new Menu(directory); + menu.addMenuListener(this); + + final JPanel placeholderPanel = panels.get(newMenuHorizontalIndex); + final JPanel menuPanel = menu.getMenuPanel(); + placeholderPanel.add(menuPanel, BorderLayout.CENTER); + + menus.add(menu); + return true; + } + + public JPanel buildContentPanel() { + + contentPane = new JPanel(); + contentPane.setBackground(Color.BLACK); + contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.X_AXIS)); + Utils.setComponentSize(contentPane, CONTENT_PANEL_SIZE); + + // initialize panels + for (int i = 0; i < VERTICAL_MENUS_COUNT; i++) { + contentPane.add(Box.createHorizontalStrut(10)); + + final JPanel panel = new JPanel(new BorderLayout()); + panel.setBackground(Color.BLACK); + + Utils.setComponentSize(panel, Menu.SIZE_MENU_PANEL); + + panels.add(panel); + contentPane.add(panel); + } + + final File directory = instantLauncher.configurationManager.getConfiguration().getNavigationRootDirectory(); + addMenu(directory); + + return contentPane; + } + + private int getMenuIndex(final Menu menu) { + int i = 0; + for (final Menu m : menus) { + if (m == menu) { + return i; + } + i++; + } + return -1; + } + + @Override + public void menuItemHighlighted(final Menu menu, final File chosenFile) { + + if (chosenFile.isDirectory()) { + final int menuIndex = getMenuIndex(menu); + + if (menuIndex >= 0) { + removeMenus(menuIndex + 1); + if (addMenu(chosenFile)) { + contentPane.validate(); + contentPane.repaint(); + } + } + } + + } + + @Override + public void menuItemSelected(final File chosenFile) { + if (chosenFile.isFile()) { + if (chosenFile.canExecute()) { + executeCommand(chosenFile.getAbsolutePath()); + InstantLauncher.exitProgram(); + } else { + boolean fileOpened; + try { + fileOpened = instantLauncher.openFile(chosenFile.getCanonicalFile()); + if (fileOpened) + InstantLauncher.exitProgram(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + if (chosenFile.isDirectory()) { + try { + instantLauncher.openDirectory(chosenFile.getCanonicalFile()); + InstantLauncher.exitProgram(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void menuItemSelectedAlternative(final File chosenFile) { + if (chosenFile.isFile()) + chosenFile.setExecutable(!chosenFile.canExecute()); + else { + instantLauncher.openDirectoryInTerminal(chosenFile); + InstantLauncher.exitProgram(); + } + } + + private void removeMenus(final int fromIndex) { + + for (int i = fromIndex; i < menus.size(); i++) { + final JPanel jPanel = panels.get(i); + jPanel.removeAll(); + } + + while (menus.size() > fromIndex) { + menus.remove(fromIndex); + } + } + + @Override + public void menuItemSelectedAlternative2(File chosenFile) { + // TODO: define some middle mouse click functions + } + + +} diff --git a/src/test/resources/exampleMenu/Moon.jpeg b/src/test/resources/exampleMenu/Moon.jpeg new file mode 100644 index 0000000..c3beeac Binary files /dev/null and b/src/test/resources/exampleMenu/Moon.jpeg differ diff --git a/src/test/resources/exampleMenu/another directory/sample.txt b/src/test/resources/exampleMenu/another directory/sample.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/exampleMenu/broken link b/src/test/resources/exampleMenu/broken link new file mode 120000 index 0000000..86a410d --- /dev/null +++ b/src/test/resources/exampleMenu/broken link @@ -0,0 +1 @@ +broken \ No newline at end of file diff --git a/src/test/resources/exampleMenu/chosen subdirectory/Sunrise over Tallinn.jpeg b/src/test/resources/exampleMenu/chosen subdirectory/Sunrise over Tallinn.jpeg new file mode 100644 index 0000000..33d6eef Binary files /dev/null and b/src/test/resources/exampleMenu/chosen subdirectory/Sunrise over Tallinn.jpeg differ diff --git a/src/test/resources/exampleMenu/chosen subdirectory/sample.txt b/src/test/resources/exampleMenu/chosen subdirectory/sample.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/exampleMenu/executable.sh b/src/test/resources/exampleMenu/executable.sh new file mode 100755 index 0000000..8bb9f02 --- /dev/null +++ b/src/test/resources/exampleMenu/executable.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +xclock + + + diff --git a/src/test/resources/exampleMenu/sample link b/src/test/resources/exampleMenu/sample link new file mode 120000 index 0000000..454bc28 --- /dev/null +++ b/src/test/resources/exampleMenu/sample link @@ -0,0 +1 @@ +another directory/ \ No newline at end of file