├── .gitignore ├── README.md ├── TheGURQ.md ├── ToDo.txt ├── build.exe.xml ├── build.properties ├── build.xml ├── java └── org │ └── tmotte │ ├── common │ ├── io │ │ └── Loader.java │ ├── swang │ │ ├── CurrentOS.java │ │ ├── GridBug.java │ │ ├── KeyMapper.java │ │ ├── KeyParser.java │ │ ├── MenuUtils.java │ │ ├── MinimumFont.java │ │ ├── Radios.java │ │ ├── SimpleClipboard.java │ │ └── SpinnerFix.java │ └── text │ │ ├── Appender.java │ │ ├── DelimitedString.java │ │ ├── StackTracer.java │ │ └── StringChunker.java │ └── klonk │ ├── Editor.java │ ├── EditorListener.java │ ├── Menus.java │ ├── config │ ├── BootContext.java │ ├── KHome.java │ ├── KPersist.java │ ├── Klonk.java │ ├── PopupInfo.java │ ├── msg │ │ ├── Editors.java │ │ ├── MainDisplay.java │ │ ├── Setter.java │ │ ├── StatusUpdate.java │ │ ├── UserNotify.java │ │ └── UserServer.java │ └── option │ │ ├── DelimiterOpts.java │ │ ├── EncryptionOptions.java │ │ ├── FontOptions.java │ │ ├── SSHOptions.java │ │ └── TabAndIndentOptions.java │ ├── controller │ ├── ControllerUtils.java │ ├── CtrlFavorites.java │ ├── CtrlFileOther.java │ ├── CtrlMain.java │ ├── CtrlMarks.java │ ├── CtrlOptions.java │ ├── CtrlOther.java │ ├── CtrlSearch.java │ ├── CtrlSelection.java │ ├── CtrlUndo.java │ └── Recents.java │ ├── edit │ ├── Indenter.java │ ├── IndenterTest.java │ ├── MyCaret.java │ ├── MyTextArea.java │ ├── Selectable.java │ ├── Spaceable.java │ ├── Undo.java │ ├── UndoEvent.java │ ├── UndoListener.java │ ├── UndoSimilar.java │ └── UndoStep.java │ ├── io │ ├── EncryptionDecryptionStream.java │ ├── EncryptionParams.java │ ├── EncryptionStream.java │ ├── FileListen.java │ ├── FileListenMemoryMap.java │ ├── FileMetaData.java │ ├── KFileIO.java │ ├── KLog.java │ ├── LightweightWriter.java │ ├── LockInterface.java │ ├── LockTest.java │ ├── Locker.java │ └── Printing.java │ ├── ssh │ ├── ConnectionParse.java │ ├── IFileGet.java │ ├── IUserPass.java │ ├── MeatCounter.java │ ├── SFTP.java │ ├── SSH.java │ ├── SSHCommandLine.java │ ├── SSHConnections.java │ ├── SSHExec.java │ ├── SSHExecResult.java │ ├── SSHFile.java │ ├── SSHFileAttr.java │ ├── WrapMap.java │ └── test │ │ ├── FileListing.java │ │ ├── FileSave.java │ │ ├── TestCaching.java │ │ ├── TestLocking.java │ │ └── TestTilde.java │ └── windows │ ├── MainLayout.java │ ├── Positioner.java │ └── popup │ ├── About-Version.txt │ ├── About.java │ ├── EncryptionInput.java │ ├── Favorites.java │ ├── FileDialogWrapper.java │ ├── FileFind.java │ ├── FindAndReplace.java │ ├── Finder.java │ ├── FontPicker.java │ ├── GoToLine.java │ ├── Help.html │ ├── Help.java │ ├── KAlert.java │ ├── LineDelimiterListener.java │ ├── LineDelimiters.java │ ├── OpenFileList.java │ ├── PopupTestContext.java │ ├── Shell.java │ ├── ShellCommandParser.java │ ├── TabsAndIndents.java │ ├── YesNoCancel.java │ ├── YesNoCancelAnswer.java │ └── ssh │ ├── SSHFileDialogNoFileException.java │ ├── SSHFileSystemView.java │ ├── SSHFileView.java │ ├── SSHLogin.java │ ├── SSHOpenFrom.java │ ├── SSHOpenFromResult.java │ ├── SSHOptionPicker.java │ └── Test.java ├── lib ├── Thumbs.db ├── app-find-replace.png ├── app.icns ├── app.ico ├── app.png ├── apptest-find-replace.png ├── apptest.png ├── classpath.sh ├── config.jsmooth ├── jar │ ├── jsch-0.1.51.jar │ ├── jsch-0.1.53.jar.broken │ └── vngx-jsch-0.10.jar.unused ├── makedmg ├── makeexe └── makewin.bat ├── license.html ├── script ├── build.sh ├── publish.sh ├── testAny.sh ├── testApp.sh ├── testAppCygwin.sh └── testLock.sh └── test ├── CR.txt ├── CRLF.txt ├── LF.txt ├── MS-UTF16-BE.txt ├── MS-UTF16-LE.txt ├── MS-UTF8.txt └── autoopen ├── 10.aaa ├── 11.aaa ├── 12.aaa ├── 13.aaa ├── 14.aaa ├── 15.aaa ├── 16.aaa ├── 17.aaa ├── 18.aaa ├── 2.aaa ├── 3.aaa ├── 4.aaa ├── 5.aaa ├── 6.aaa ├── 7.aaa ├── 8.aaa └── 9.aaa /.gitignore: -------------------------------------------------------------------------------- 1 | test/home 2 | test/autoopen 3 | build 4 | dist 5 | .DS_Store 6 | Klonk-1.0.dmg 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Klonk 2 | Klonk is a simple but robust text editor that aims for fluid ease-of-use and convenience over exotic features. I wrote it for myself and use it as my everyday editor. 3 | 4 | Capabilities worth mentioning: 5 | 6 | * 100% Java as well as convenient OSX/MacOS & Windows executables 7 | * Keyboard-friendly but equally menu-friendly 8 | * Dirt-simple undo/redo that allows [recovery of all edit states](./TheGURQ.md) from beginning to end 9 | * Open files over SSH 10 | * File encryption 11 | * Execute shell scripts without switching applications 12 | * The usual things: Trailing-whitespace-trim, auto-indent with tabs or spaces, line wrap control, sorting, marking, alignment tricks, multi-line find & replace with regex, and so forth 13 | 14 | # Download 15 | [Download zip file here](https://zaboople.github.io/pages/klonk/index.html). Includes jar & Windows .exe files (for MacOS installation refer to "Building it" below). Note that the Windows .exe does not include the required Java installation on your computer. We've been building for Java 16 lately. 16 | 17 | # Building it 18 | To build, use a Java 9+ JDK and a reasonably recent version of [Apache Ant](http://ant.apache.org/). 19 | 20 | # Building and running natively 21 | 22 | ## Windows 23 | To build a Windows Klonk.exe you will need [JSmooth](http://jsmooth.sourceforge.net/). Type `ant help` in the git checkout directory for detailed instructions. Note that because the executable does not contain a Java virtual machine of its own, it needs to find one on your computer. You can go to: 24 | - Desktop 25 | - Right click "My Computer" (or whatever it's named) 26 | - Click "Properties 27 | - Select the "Advanced" tab 28 | - Click "Environment Variables" 29 | - Under "System variables" click "New" 30 | - For Variable Name, enter "JAVA_HOME"; 31 | - For Variable Value, enter the path of your computer's java install, e.g. "c:\Program Files\Java\jre-9.0.1" 32 | - And click "Apply" or "Save" 33 | 34 | ## Macintosh 35 | OSX/MacOS native executables are supported via Java's built-in `javapackager` utility. Refer to the script [lib/makedmg](lib/makedmg). Note that this script includes a `-Bruntime=` flag that tells javapackager not to put a java JRE into the install, and this only works correctly when building for java 1.8.0_92 and above. You need to remove it for earlier versions of Java 8 (or just upgrade to the latest). 36 | -------------------------------------------------------------------------------- /build.exe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Using JSmooth home: ${env.JSMOOTH_HOME} 6 | 7 | 8 | 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /build.properties: -------------------------------------------------------------------------------- 1 | # Version 2.5.3 fixes multi-screen display issues, and bad behavior when ctrl-shift-hopping. 2 | # Version 2.5.4 fixes problem with shell interfering with user's typing after command execution 3 | # Version 2.5.5 makes it easy to match tab indent & space indent sizes 4 | # Version 2.6.0 Fixes compiler warnings and implements file find 5 | # Version 2.6.1 Has fixes for file find, and file-find-exclude 6 | # Version 2.6.2 Has fixes for file find 7 | # Version 2.6.3 Has fixes for file find 8 | # Version 2.6.4 Has undo/redo tweaks, mainly Undo-To-History-Switch 9 | # Version 2.6.5 Switches to Process.destroyForcibly from Process.destroy 10 | # Version 2.6.6 Add delete to file menu 11 | # Version 2.7 Make compatible with JDK 21, latest MacOS & OpenJDK jpackage, fix loads of compiler warnings 12 | VERSION.KLONK=2.7 13 | -------------------------------------------------------------------------------- /java/org/tmotte/common/io/Loader.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.common.io; 2 | import java.io.InputStream; 3 | import java.io.InputStreamReader; 4 | import java.io.BufferedReader; 5 | 6 | /** 7 | * A convenience class for loading data from InputStreams. 8 | */ 9 | public class Loader { 10 | public static String loadUTF8String(java.lang.Class c, String fileName) { 11 | try { 12 | java.io.InputStream is=c.getResourceAsStream(fileName); 13 | return loadUTF8String(is); 14 | } catch (Exception e) { 15 | throw new RuntimeException(e); 16 | } 17 | } 18 | /** 19 | * Invokes loadString() with approxlen parameter of 512. 20 | */ 21 | public static String loadUTF8String(java.io.InputStream ios) throws Exception{ 22 | return loadString(ios, "utf-8", 512); 23 | } 24 | 25 | /** 26 | * Loads a String from an input stream. 27 | * @param approxlen The size of the byte buffer to use when loading. 28 | */ 29 | public static String loadString(java.io.InputStream ios, String encoding, int approxlen) throws Exception{ 30 | try { 31 | InputStreamReader br=new InputStreamReader(ios, encoding); 32 | StringBuffer buffer=new StringBuffer(); 33 | char[] readBuffer=new char[Math.min(4096, approxlen)]; 34 | try { 35 | int charsRead; 36 | while ((charsRead=br.read(readBuffer, 0, readBuffer.length))>0) 37 | buffer.append(readBuffer, 0, charsRead); 38 | } finally { 39 | ios.close(); 40 | } 41 | return buffer.toString(); 42 | } finally { 43 | ios.close(); 44 | } 45 | } 46 | 47 | /** 48 | * Creates a byte array of the specifed size and loads it from the given InputStream. The stream 49 | * will be closed at completion, even if there are more bytes to read. 50 | */ 51 | public static byte[] loadBytes(java.io.InputStream ios, int size) throws Exception{ 52 | try { 53 | byte[] bytes=new byte[size]; 54 | int off=0; 55 | while (off-1; 15 | isOSX=os.indexOf("mac os")>-1; 16 | } 17 | public CurrentOS(boolean isOSX, boolean isMSWindows) { 18 | this.isOSX=isOSX; 19 | this.isMSWindows=isMSWindows; 20 | } 21 | public void fixEnterKey(JButton jb, Action action) { 22 | if (isOSX) 23 | jb.addKeyListener( 24 | new KeyAdapter() { 25 | public void keyPressed(KeyEvent e){ 26 | if (e.getKeyCode()==KeyEvent.VK_ENTER) { 27 | e.consume(); 28 | action.actionPerformed(new ActionEvent(jb, 1, "faked")); 29 | } 30 | } 31 | } 32 | ); 33 | } 34 | } -------------------------------------------------------------------------------- /java/org/tmotte/common/swang/GridBug.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.common.swang; 2 | 3 | import java.awt.Component; 4 | import java.awt.Container; 5 | import java.awt.Dimension; 6 | import java.awt.Insets; 7 | import java.awt.Font; 8 | import java.awt.GridBagConstraints; 9 | import java.awt.GridBagLayout; 10 | import java.awt.Point; 11 | 12 | /** 13 | * Use gridwidth and gridheight to make a component span extra columns/rows 14 | * Use gridx and gridy to control position; also addX() and addY() 15 | * Use fill + weightx and weighty to control expansion; also setFill() and weightXY() 16 | * Finally use insets to control padding; also setInsets() 17 | */ 18 | public class GridBug extends GridBagConstraints { 19 | private static final long serialVersionUID = 1L; 20 | public GridBagLayout gbl=new GridBagLayout(); 21 | public Container container; 22 | 23 | public GridBug(Container c) { 24 | this.container=c; 25 | container.setLayout(gbl); 26 | gridx=1; 27 | gridy=1; 28 | } 29 | 30 | public Container getContainer() { 31 | return container; 32 | } 33 | public GridBug add(Component c) { 34 | gbl.setConstraints(c, this); 35 | container.add(c); 36 | return this; 37 | } 38 | 39 | public GridBug setInsets(int i) { 40 | return setInsets(i, i, i, i); 41 | } 42 | public GridBug setInsets(int top, int right, int bottom, int left) { 43 | insets.top=top; 44 | insets.right=right; 45 | insets.bottom=bottom; 46 | insets.left=left; 47 | return this; 48 | } 49 | public GridBug insets(int i) { 50 | return setInsets(i, i, i, i); 51 | } 52 | public GridBug insets(int top, int right, int bottom, int left) { 53 | return setInsets(top, right, bottom, left); 54 | } 55 | public GridBug insetTop(int inset) {insets.top=inset; return this;} 56 | public GridBug insetRight(int inset) {insets.right=inset; return this;} 57 | public GridBug insetBottom(int inset) {insets.bottom=inset; return this;} 58 | public GridBug insetLeft(int inset) {insets.left=inset; return this;} 59 | 60 | public GridBug setY(int gridy){ 61 | this.gridy=gridy; 62 | return this; 63 | } 64 | public GridBug y(int gridy){ 65 | this.gridy=gridy; 66 | return this; 67 | } 68 | public GridBug addY(Component... cs) { 69 | for (Component c: cs) 70 | addY(c); 71 | return this; 72 | } 73 | public GridBug addY(Component c) { 74 | if (container.getComponentCount()!=0) 75 | gridy++; 76 | return add(c); 77 | } 78 | 79 | public GridBug setX(int gridx){ 80 | this.gridx=gridx; 81 | return this; 82 | } 83 | public GridBug x(int gridx){ 84 | this.gridx=gridx; 85 | return this; 86 | } 87 | public GridBug addX(Component... cs) { 88 | for (Component c: cs) 89 | addX(c); 90 | return this; 91 | } 92 | public GridBug addX(Component c) { 93 | if (container.getComponentCount()!=0) 94 | gridx++; 95 | return add(c); 96 | } 97 | 98 | public GridBug weightXY(double x, double y) { 99 | weightx=x; weighty=y; return this; 100 | } 101 | public GridBug weightXY(double xy) { 102 | return weightXY(xy, xy); 103 | } 104 | public GridBug weightX(double x) { 105 | weightx=x; return this; 106 | } 107 | public GridBug weightY(double y) { 108 | weighty=y; return this; 109 | } 110 | public GridBug gridXY(int x, int y) { 111 | gridx=x; gridy=y; return this; 112 | } 113 | public GridBug gridXY(int xy) { 114 | return gridXY(xy, xy); 115 | } 116 | public GridBug gridX(int x) { 117 | this.gridx=x; return this; 118 | } 119 | public GridBug gridY(int y) { 120 | this.gridy=y; return this; 121 | } 122 | public GridBug setFill(int fill) { 123 | this.fill=fill; 124 | return this; 125 | } 126 | public GridBug fill(int fill) { 127 | this.fill=fill; return this; 128 | } 129 | public GridBug anchor(int anchor) { 130 | this.anchor=anchor; return this; 131 | } 132 | public GridBug gridWidth(int gridWidth) { 133 | this.gridwidth=gridWidth; return this; 134 | } 135 | public GridBug gridHeight(int gridHeight) { 136 | this.gridheight=gridHeight; return this; 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /java/org/tmotte/common/swang/KeyMapper.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.common.swang; 2 | import java.awt.Toolkit; 3 | import java.awt.event.KeyEvent; 4 | import java.awt.event.ActionListener; 5 | import java.awt.event.ActionEvent; 6 | import javax.swing.AbstractAction; 7 | import javax.swing.AbstractButton; 8 | import javax.swing.Action; 9 | import javax.swing.JButton; 10 | import javax.swing.JComponent; 11 | import javax.swing.KeyStroke; 12 | 13 | public class KeyMapper { 14 | 15 | static Toolkit toolkit=Toolkit.getDefaultToolkit(); 16 | 17 | ////////////////////////////////////////////////////////// 18 | // ADDING HOTKEYS TO COMPONENTS; NOTE HOW THE COMPONENT // 19 | // ITSELF IS RETURNED FOR CONVENIENCE: // 20 | ////////////////////////////////////////////////////////// 21 | 22 | public static Action makeAction(ActionListener listener) { 23 | return new AbstractAction() { 24 | public void actionPerformed(ActionEvent event) {listener.actionPerformed(event);} 25 | }; 26 | } 27 | public static AbstractButton accel(AbstractButton jc, ActionListener listener, KeyStroke hotKey) { 28 | return accel(jc, makeAction(listener), hotKey); 29 | } 30 | public static AbstractButton accel(AbstractButton jc, Action action, KeyStroke hotKey) { 31 | String text=jc.getText(); 32 | if (text.length()>20) 33 | text=text.substring(0,20); 34 | text+="-"+hotKey; 35 | accel((JComponent)jc, text, action, hotKey); 36 | return jc; 37 | } 38 | public static AbstractButton accel(AbstractButton jc, Action action, int hotKey) { 39 | return accel(jc, action, key(hotKey)); 40 | } 41 | public static AbstractButton accel(AbstractButton jc, Action action, int hotKey, int modifiers) { 42 | return accel(jc, action, key(hotKey, modifiers)); 43 | } 44 | public static AbstractButton accel(AbstractButton jc, Action action, int hotKey, int... modifiers) { 45 | return accel(jc, action, key(hotKey, modifiers)); 46 | } 47 | 48 | public static JComponent accel(JComponent jc, Action action, KeyStroke hotKey) { 49 | return accel(jc, String.valueOf(jc.hashCode()), action, hotKey); 50 | } 51 | public static JComponent accel(JComponent jc, String stupidName, Action action, KeyStroke hotKey) { 52 | action.putValue(Action.ACCELERATOR_KEY, hotKey); 53 | jc.getActionMap() 54 | .put(stupidName, action); 55 | jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) 56 | .put(hotKey, stupidName); 57 | return jc; 58 | } 59 | public static JComponent accel(JComponent jc, String stupidName, Action action, int hotKey) { 60 | return accel(jc, stupidName, action, key(hotKey)); 61 | } 62 | public static JComponent accel(JComponent jc, String stupidName, Action action, int hotKey, int modifiers) { 63 | return accel(jc, stupidName, action, key(hotKey, modifiers)); 64 | } 65 | public static JComponent accel(JComponent jc, String stupidName, Action action, int hotKey, int... modifiers) { 66 | return accel(jc, stupidName, action, key(hotKey, modifiers)); 67 | } 68 | 69 | ///////////////////////////////////// 70 | // DETECTING MODIFIER KEY PRESSES: // 71 | ///////////////////////////////////// 72 | 73 | public static boolean ctrlPressed(KeyEvent k) { 74 | return ctrlPressed(k.getModifiersEx()); 75 | } 76 | public static boolean ctrlPressed(int mods) { 77 | return (mods & KeyEvent.CTRL_DOWN_MASK)==KeyEvent.CTRL_DOWN_MASK; 78 | } 79 | public static boolean shiftPressed(KeyEvent k) { 80 | return shiftPressed(k.getModifiersEx()); 81 | } 82 | public static boolean shiftPressed(int mods) { 83 | return (mods & KeyEvent.SHIFT_DOWN_MASK)==KeyEvent.SHIFT_DOWN_MASK; 84 | } 85 | public static boolean altPressed(KeyEvent k) { 86 | return altPressed(k.getModifiersEx()); 87 | } 88 | public static boolean altPressed(int mods) { 89 | return (mods & KeyEvent.ALT_DOWN_MASK)==KeyEvent.ALT_DOWN_MASK; 90 | } 91 | 92 | /** This is the macintosh "command" key */ 93 | public static boolean metaPressed(int mods, CurrentOS currentOS) { 94 | return currentOS.isOSX && (mods & KeyEvent.META_DOWN_MASK)==KeyEvent.META_DOWN_MASK; 95 | } 96 | public static boolean metaPressed(KeyEvent k, CurrentOS currentOS) { 97 | return currentOS.isOSX && metaPressed(k.getModifiersEx(), currentOS); 98 | } 99 | /** This is the macintosh "option" key */ 100 | public static boolean optionPressed(int mods, CurrentOS currentOS) { 101 | return currentOS.isOSX && (mods & KeyEvent.ALT_DOWN_MASK)==KeyEvent.ALT_DOWN_MASK; 102 | } 103 | public static boolean optionPressed(KeyEvent k, CurrentOS currentOS) { 104 | return currentOS.isOSX && optionPressed(k.getModifiersEx(), currentOS); 105 | } 106 | 107 | /** 108 | * Tells us if the default modifier for the OS (Ctrl or Command or what) 109 | * was pressed. 110 | */ 111 | public static boolean modifierPressed(int mods, CurrentOS currentOS) { 112 | int s=currentOS.isOSX 113 | ?KeyEvent.META_DOWN_MASK 114 | :KeyEvent.CTRL_DOWN_MASK; 115 | return (mods & s) == s; 116 | } 117 | public static boolean modifierPressed(KeyEvent k, CurrentOS currentOS) { 118 | return modifierPressed(k.getModifiersEx(), currentOS); 119 | } 120 | 121 | /////////////////////////////// 122 | // CREATE KeyStroke OBJECTS: // 123 | /////////////////////////////// 124 | 125 | /** 126 | * Gives us the menu shortcut key - ALT or COMMAND or whatever. 127 | */ 128 | public static int shortcutByOS(){ 129 | return toolkit.getMenuShortcutKeyMaskEx(); 130 | } 131 | public static KeyStroke keyByOS(int hotKey) { 132 | return KeyStroke.getKeyStroke(hotKey, shortcutByOS()); 133 | } 134 | public static KeyStroke keyByOS(int hotKey, int extra) { 135 | return KeyStroke.getKeyStroke(hotKey, shortcutByOS() | extra); 136 | } 137 | public static KeyStroke key(int hotKey) { 138 | return KeyStroke.getKeyStroke(hotKey, 0); 139 | } 140 | /** 141 | * @param modifiers Note that this is not KeyEvent.VK_etc, but InputEvent.etc_MASK 142 | */ 143 | public static KeyStroke key(int hotKey, int modifiers) { 144 | return KeyStroke.getKeyStroke(hotKey, modifiers); 145 | } 146 | public static KeyStroke key(int hotKey, int... modifiers) { 147 | int mod=0; 148 | for (int m: modifiers) 149 | mod|=m; 150 | return key(hotKey, mod); 151 | } 152 | public static void easyCancel(JButton btn, ActionListener action){ 153 | easyCancel(btn, makeAction(action)); 154 | } 155 | public static void easyCancel(JButton btn, Action action){ 156 | KeyMapper.accel(btn, action, KeyMapper.key(KeyEvent.VK_ESCAPE)); 157 | KeyMapper.accel(btn, action, KeyMapper.keyByOS(KeyEvent.VK_W)); 158 | KeyMapper.accel(btn, action, KeyMapper.key(KeyEvent.VK_F4, KeyEvent.ALT_DOWN_MASK)); 159 | } 160 | } -------------------------------------------------------------------------------- /java/org/tmotte/common/swang/KeyParser.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.common.swang; 2 | import javax.swing.KeyStroke; 3 | import java.awt.event.InputEvent; 4 | import java.awt.event.KeyEvent; 5 | import java.util.regex.Pattern; 6 | import java.lang.reflect.Field; 7 | import java.util.Map; 8 | import java.util.HashMap; 9 | 10 | /** 11 | * This is just for mapping shortcut keys to and from a text representation. 12 | */ 13 | public class KeyParser { 14 | Map keyMap=new HashMap<>(); 15 | public KeyParser() { 16 | for (Field field: KeyEvent.class.getFields()) 17 | try { 18 | Object o=field.get(field); 19 | String name=field.getName(); 20 | if (name.startsWith("VK_") && o instanceof Integer) 21 | keyMap.put(o, name); 22 | } catch (Exception e) { 23 | throw new RuntimeException("Error processing field "+field.getName()+" "+field); 24 | } 25 | } 26 | public String toString(KeyStroke ks) { 27 | int mods=ks.getModifiers(), kc=ks.getKeyCode(); 28 | String keyName=keyMap.get(kc); 29 | if (keyName==null) 30 | throw new RuntimeException("Can't parse key code "+kc+" from "+ks); 31 | String modName=""; 32 | if (mods==0) 33 | modName=""; 34 | if ((mods & KeyEvent.CTRL_DOWN_MASK) != 0) 35 | modName+=" ctrl"; 36 | if ((mods & KeyEvent.META_DOWN_MASK) != 0) 37 | modName+=" command"; 38 | if ((mods & KeyEvent.ALT_DOWN_MASK) != 0) 39 | modName+=" alt/option"; 40 | if ((mods & KeyEvent.SHIFT_DOWN_MASK) != 0) 41 | modName+=" shift"; 42 | return (modName+" "+keyName).trim(); 43 | } 44 | 45 | 46 | private static Pattern dividerRegex=Pattern.compile("(-| )"); 47 | 48 | public static KeyStroke parse(String parse) throws Exception { 49 | try { 50 | int mods=0; 51 | int hotkey=0; 52 | for (String next : dividerRegex.split(parse)) { 53 | next=next.trim(); 54 | if (!next.equals("")) { 55 | int mod=getMod(next); 56 | if (mod!=0) 57 | mods|=mod; 58 | else 59 | if (hotkey!=0) 60 | throw new RuntimeException("More than two keyboard characters specified (multiple modifiers are OK), cannot accept: "+next); 61 | else 62 | hotkey=getKey(next); 63 | } 64 | } 65 | return KeyMapper.key(hotkey, mods); 66 | } catch (Exception e) { 67 | throw new RuntimeException( 68 | "Failed to parse \""+parse+"\", original error was: "+e, 69 | e 70 | ); 71 | } 72 | } 73 | 74 | 75 | private static int getMod(String s) { 76 | String modName=s.toLowerCase(); 77 | int downMask=modName.indexOf("_down_mask"); 78 | if (downMask > 0) 79 | modName=modName.substring(0, downMask); 80 | if (modName.equals("ctrl") || modName.equals("control")) 81 | return KeyEvent.CTRL_DOWN_MASK; 82 | else 83 | if (modName.equals("command") || modName.equals("cmd") || modName.equals("meta")) 84 | return KeyEvent.META_DOWN_MASK; 85 | else 86 | if (modName.equals("option") || modName.equals("alt") || modName.equals("opt") || modName.equals("alt/option")) 87 | //Not a bug - on mac, option is alt. Don't ask me. 88 | return KeyEvent.ALT_DOWN_MASK; 89 | else 90 | if (modName.equals("shift")) 91 | return KeyEvent.SHIFT_DOWN_MASK; 92 | else 93 | return 0; 94 | } 95 | private static int getKey(String original) { 96 | String keyName=original.toUpperCase(); 97 | if (!keyName.startsWith("VK_")) 98 | keyName="VK_"+keyName; 99 | try { 100 | Field field=KeyEvent.class.getField(keyName); 101 | return field.getInt(field); 102 | } catch (Exception e) { 103 | throw new RuntimeException( 104 | "Could not find key that maps to \""+keyName 105 | +"\" derived from \""+original+"\", " 106 | +"you may want to consult with " 107 | +"the java documentation for the java.awt.event.KeyEvent class; original error: " 108 | +e.getMessage(), 109 | e 110 | ); 111 | } 112 | } 113 | public static void main(String[] args) throws Exception { 114 | KeyParser parser=new KeyParser(); 115 | for (String arg: args) { 116 | KeyStroke parsed=parse(arg); 117 | String reversed=parser.toString(parsed); 118 | KeyStroke parsedAgain=parse(reversed); 119 | System.out.println("From: \""+arg+"\"\n to: "+parsed+"\n to: \""+reversed+"\"\n to: "+parsedAgain); 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /java/org/tmotte/common/swang/MenuUtils.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.common.swang; 2 | import javax.swing.Action; 3 | import javax.swing.AbstractAction; 4 | import java.awt.event.ActionListener; 5 | import javax.swing.event.MenuListener; 6 | import javax.swing.JCheckBoxMenuItem; 7 | import javax.swing.JMenu; 8 | import javax.swing.JMenuBar; 9 | import javax.swing.JMenuItem; 10 | import javax.swing.KeyStroke; 11 | import javax.swing.JPopupMenu; 12 | 13 | public class MenuUtils { 14 | 15 | public static JMenu add(JMenu menu, JMenuItem... items) { 16 | for (JMenuItem i: items) 17 | if (i!=null) 18 | menu.add(i); 19 | return menu; 20 | } 21 | 22 | public static JPopupMenu add(JPopupMenu menu, JMenuItem... items) { 23 | for (JMenuItem i: items) 24 | if (i!=null) 25 | menu.add(i); 26 | return menu; 27 | } 28 | 29 | public static JMenu doMenu(JMenuBar bar, String title, MenuListener listener, int mnemon) { 30 | JMenu f=doMenu(title, listener, mnemon); 31 | bar.add(f); 32 | return f; 33 | } 34 | public static JMenu doMenu(JMenuBar bar, String title, int mnemon) { 35 | return doMenu(bar, title, null, mnemon); 36 | } 37 | 38 | public static JMenu doMenu(String title, MenuListener listener, int mnemon) { 39 | JMenu f=new JMenu(title); 40 | if (mnemon>-1) 41 | f.setMnemonic(mnemon); 42 | if (listener!=null) 43 | f.addMenuListener(listener); 44 | return f; 45 | } 46 | public static JMenu doMenu(String title, int mnemon) { 47 | return doMenu(title, null, mnemon); 48 | } 49 | 50 | 51 | public static JMenuItem doMenuItem(String title, ActionListener listener) { 52 | return doMenuItem(title, listener, -1, null); 53 | } 54 | public static JMenuItem doMenuItem(String title, ActionListener listener, int mnemon) { 55 | return doMenuItem(title, listener, mnemon, null); 56 | } 57 | public static JMenuItem doMenuItem(String title, ActionListener listener, int mnemon, KeyStroke shortcut) { 58 | JMenuItem j=new JMenuItem(title); 59 | if (mnemon>-1) 60 | j.setMnemonic(mnemon); 61 | if (listener!=null) 62 | j.addActionListener(listener); 63 | if (shortcut!=null){ 64 | //Both of the following *should* work alone, but each is flawed unless we use them together: 65 | //- Without the first the accelerator key won't show up in the menu; 66 | //- Without the second the response time will be much slower 67 | // for heavily/repetitively used accelerators. 68 | j.setAccelerator(shortcut); 69 | KeyMapper.accel(j, listener, shortcut); 70 | } 71 | return j; 72 | } 73 | 74 | 75 | public static JCheckBoxMenuItem doMenuItemCheckbox(String title, ActionListener listener) { 76 | return doMenuItemCheckbox(title, listener, -1, true); 77 | } 78 | public static JCheckBoxMenuItem doMenuItemCheckbox(String title, ActionListener listener, int mnemon, boolean isOn) { 79 | JCheckBoxMenuItem j=new JCheckBoxMenuItem(title, true); 80 | if (mnemon>-1) 81 | j.setMnemonic(mnemon); 82 | if (listener!=null) 83 | j.addActionListener(listener); 84 | j.setState(isOn); 85 | return j; 86 | } 87 | 88 | 89 | 90 | } -------------------------------------------------------------------------------- /java/org/tmotte/common/swang/MinimumFont.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.common.swang; 2 | import java.awt.Component; 3 | import java.awt.Container; 4 | import java.awt.Font; 5 | import java.awt.Label; 6 | import javax.swing.JComponent; 7 | import javax.swing.JLabel; 8 | import javax.swing.JMenu; 9 | import java.util.Set; 10 | 11 | /** 12 | * Allows me to increase the font for general-purpose controls 13 | * to a manageable size because Swing defaults to stupid tiny. 14 | * Just uses a JLabel to establish the system default font, 15 | * and derives a "more reasonable" font from that. 16 | */ 17 | public class MinimumFont { 18 | 19 | /////////////////////////////// 20 | // Initialization & Get/Set: // 21 | /////////////////////////////// 22 | 23 | Font font; 24 | Font fontBold; 25 | 26 | public MinimumFont(int size) { 27 | this(getFont(size)); 28 | } 29 | private MinimumFont(Font font) { 30 | setFont(font); 31 | } 32 | 33 | public void setSize(int size) { 34 | setFont(getFont(size, font)); 35 | } 36 | public int getSize() { 37 | return font.getSize(); 38 | } 39 | 40 | /////////////////////////////////////// 41 | // Public font assignment functions: // 42 | /////////////////////////////////////// 43 | 44 | public void set(JComponent... jcs) { 45 | for (JComponent jc : jcs) 46 | set(jc); 47 | } 48 | public void set(JComponent jc) { 49 | set(jc, null); 50 | } 51 | public void set(Component jc) { 52 | set(jc, null); 53 | } 54 | public void set(Component c, Set ignore) { 55 | if (c==null) 56 | //Nulls happen during normal traversal, deal with it: 57 | return; 58 | if (ignore!=null && ignore.contains(c)) 59 | return; 60 | setFont(c); 61 | if (c instanceof JMenu) 62 | //This is necessary because at least on OSX the JComponent/Container 63 | //functions frequently come up empty: 64 | expand((JMenu)c, ignore); 65 | else 66 | if (c instanceof JComponent) 67 | expand((JComponent)c, ignore); 68 | else 69 | if (c instanceof Container) 70 | expand((Container)c, ignore); 71 | } 72 | 73 | //////////////////////////////////////// 74 | // Private font assignment functions: // 75 | //////////////////////////////////////// 76 | 77 | private void expand(JComponent jc, Set ignore) { 78 | if (jc.getComponentCount() > 0) 79 | for (Component c : jc.getComponents()) 80 | set(c, ignore); 81 | } 82 | private void expand(Container jc, Set ignore) { 83 | if (jc.getComponentCount() > 0) 84 | for (Component c : jc.getComponents()) 85 | set(c, ignore); 86 | } 87 | private void expand(javax.swing.JMenu j, Set ignore) { 88 | int count=j.getItemCount(); 89 | for (int i=0; i en=bg.getElements(); en.hasMoreElements();) 45 | butts[i++]=en.nextElement(); 46 | } 47 | for (int i=0; i0 ?butts[i-1] :butts[butts.length-1]; 51 | butts[i].addKeyListener( 52 | new KeyAdapter(){ 53 | public void keyPressed(KeyEvent keyEvent) { 54 | final int code=keyEvent.getKeyCode(); 55 | if (code==KeyEvent.VK_DOWN && upDown && nextButt!=null) 56 | nextButt.grabFocus(); 57 | else 58 | if (code==KeyEvent.VK_UP && upDown && prevButt!=null) 59 | prevButt.grabFocus(); 60 | else 61 | if (code==KeyEvent.VK_RIGHT && !upDown && nextButt!=null) 62 | nextButt.grabFocus(); 63 | else 64 | if (code==KeyEvent.VK_LEFT && !upDown && prevButt!=null) 65 | prevButt.grabFocus(); 66 | } 67 | } 68 | ); 69 | } 70 | return bg; 71 | } 72 | } -------------------------------------------------------------------------------- /java/org/tmotte/common/swang/SimpleClipboard.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.common.swang; 2 | import java.awt.datatransfer.Clipboard; 3 | import java.awt.datatransfer.DataFlavor; 4 | import java.awt.datatransfer.StringSelection; 5 | import java.awt.datatransfer.Transferable; 6 | import java.awt.Toolkit; 7 | 8 | public class SimpleClipboard { 9 | public static void set(String name) { 10 | StringSelection stringSelection = new StringSelection(name); 11 | Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 12 | clipboard.setContents(stringSelection, stringSelection); 13 | } 14 | public static String get() { 15 | try { 16 | Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 17 | Transferable trs=clipboard.getContents(null); 18 | Object o=null; 19 | if (trs.isDataFlavorSupported(DataFlavor.stringFlavor)) 20 | o=trs.getTransferData(DataFlavor.stringFlavor); 21 | if (o==null) 22 | o=""; 23 | return o.toString(); 24 | } catch (Exception e) { 25 | throw new RuntimeException(e); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /java/org/tmotte/common/swang/SpinnerFix.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.common.swang; 2 | import java.awt.Color; 3 | import java.awt.Component; 4 | import java.awt.Container; 5 | import java.awt.Dimension; 6 | import java.awt.Font; 7 | import java.awt.Graphics2D; 8 | import java.awt.Insets; 9 | import java.awt.Point; 10 | import java.awt.Rectangle; 11 | import java.awt.Window; 12 | import java.awt.font.FontRenderContext; 13 | import java.awt.geom.Rectangle2D; 14 | import java.awt.image.BufferedImage; 15 | import javax.swing.JSpinner; 16 | import javax.swing.SpinnerNumberModel; 17 | import org.tmotte.common.swang.Radios; 18 | 19 | /** By default JSpinner tends to take up excessive space. This allows you to re-fit it to contain a given String.*/ 20 | public class SpinnerFix { 21 | 22 | public static void fix(JSpinner jsp, Window containerWindow, String toFit) { 23 | //Required to get the component to size itself and obtain a graphics object: 24 | containerWindow.pack(); 25 | Font font=jsp.getFont(); 26 | Graphics2D gr=(Graphics2D)jsp.getGraphics(); 27 | Rectangle rect=font.getStringBounds( 28 | toFit.toCharArray(), 29 | 0, 30 | toFit.length(), 31 | gr.getFontRenderContext() 32 | ).getBounds(); 33 | jsp.getEditor().setPreferredSize( 34 | new Dimension(rect.width, rect.height) 35 | ); 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /java/org/tmotte/common/text/Appender.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.common.text; 2 | /** 3 | * This is a companion class to Appendable. By implementing this interface, a class makes it known 4 | * that it can print directly to an Appendable. This is useful when a class is designed to be 5 | * "printable", but implementing toString() would force a great deal of String concatenation. For example: 6 |
 7 |    public class MyClass implements Appender {
 8 |      String a, b, c, d, e, f;
 9 | 
10 |      //Less efficient:
11 |      public void toString() {
12 |        StringBuilder sb=new StringBuilder();
13 |        sb.append(a);
14 |        sb.append(b);
15 |        ...
16 |        sb.append(f);
17 |        return sb.toString();//Creates a large String, wasting memory.
18 |      }
19 |      
20 |      //More efficient:
21 |      public void appendTo(Appendable app) {
22 |        app.append(a);
23 |        app.append(b);
24 |        ...
25 |        app.append(f);//No additional Strings created.
26 |      }
27 |    }
28 |  
29 | */ 30 | public interface Appender { 31 | /** 32 | * When invoked, the Appender should print itself to the Appendable. 33 | */ 34 | public void appendTo(Appendable appendable) throws java.io.IOException; 35 | } -------------------------------------------------------------------------------- /java/org/tmotte/common/text/StackTracer.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.common.text; 2 | import java.io.PrintWriter; 3 | import java.io.OutputStreamWriter; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | /** Converts a java stack trace to a String as well as to Strings written to an Appendable.*/ 7 | public class StackTracer { 8 | public static String getStackTrace(Throwable t) { 9 | ByteArrayOutputStream b=new ByteArrayOutputStream(); 10 | PrintWriter pw=new PrintWriter(b); 11 | t.printStackTrace(pw); 12 | pw.flush(); 13 | return b.toString(); 14 | } 15 | public static String recurseStackTrace(Throwable t) { 16 | StringBuilder sb=new StringBuilder(); 17 | recurseStackTrace(t, sb); 18 | return sb.toString(); 19 | } 20 | public static void getStackTrace(Throwable t, Appendable s) { 21 | try { 22 | s.append(getStackTrace(t)); 23 | } catch (Exception e) { 24 | throw new RuntimeException(e); 25 | } 26 | } 27 | public static void recurseStackTrace(Throwable t, Appendable s) { 28 | try { 29 | getStackTrace(t, s); 30 | Throwable t2=t.getCause(); 31 | if (t2!=null) 32 | recurseStackTrace(t2, s); 33 | } catch (Exception e) { 34 | throw new RuntimeException(e); 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/EditorListener.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk; 2 | import java.io.File; 3 | public interface EditorListener { 4 | public void doCaretMoved(Editor ed, int caretPos); 5 | public void doCapsLock(boolean locked); 6 | public void closeEditor(); 7 | public void fileDropped(File file); 8 | public void doEditorChanged(Editor ed); 9 | } 10 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/KHome.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config; 2 | import java.io.File; 3 | 4 | public class KHome { 5 | public String dirName; 6 | public File dir; 7 | public boolean ready=false; 8 | 9 | public KHome(String directory) { 10 | dirName=directory; 11 | dir=new File(directory); 12 | if (!dir.exists()) 13 | try { 14 | dir.mkdir(); 15 | if (!dir.exists()) 16 | throw new RuntimeException("Could not make directory "+directory); 17 | } catch (Exception e) { 18 | dirName=null; 19 | dir=null; 20 | throw new RuntimeException("Could not make directory: "+directory, e); 21 | } 22 | try { 23 | dirName=dir.getCanonicalPath(); 24 | } catch (Exception e) { 25 | throw new RuntimeException(e); 26 | } 27 | ready=true; 28 | } 29 | 30 | 31 | public KHome mkdir(String newDir) { 32 | return new KHome(nameIt(dirName, newDir)); 33 | } 34 | public File nameFile(String file) { 35 | return nameFile(dirName, file); 36 | } 37 | public String toString() { 38 | return dirName; 39 | } 40 | 41 | 42 | public String getUserHome() { 43 | return dirName; 44 | } 45 | public static File nameFile(String dir, String file) { 46 | return new File(nameIt(dir, file)); 47 | } 48 | public static String nameIt(String dir, String file) { 49 | if (!dir.endsWith(File.separator)) 50 | dir+=File.separator; 51 | return dir+file; 52 | } 53 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/Klonk.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config; 2 | 3 | /** 4 | * On the macintosh the leftmost menu gets the name of the boot class, so I made this 5 | * as the bootable mac class. 6 | */ 7 | public class Klonk { 8 | public static void main(final String[] args) { 9 | BootContext.main(args); 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/PopupInfo.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config; 2 | import java.util.ArrayList; 3 | import java.util.List; 4 | import javax.swing.JFrame; 5 | import org.tmotte.common.swang.CurrentOS; 6 | import org.tmotte.klonk.config.msg.Setter; 7 | import org.tmotte.klonk.config.option.FontOptions; 8 | 9 | /** 10 | * This contains dependency-injected items that are so ubiquitous as to be practically global. 11 | */ 12 | public class PopupInfo { 13 | public final JFrame parentFrame; 14 | public final CurrentOS currentOS; 15 | private final List> fontListeners=new java.util.ArrayList<>(30); 16 | 17 | public PopupInfo(JFrame parentFrame, CurrentOS currentOS) { 18 | this.parentFrame=parentFrame; 19 | this.currentOS=currentOS; 20 | } 21 | public void addFontListener(Setter listener) { 22 | fontListeners.add(listener); 23 | } 24 | public List> getFontListeners() { 25 | return fontListeners; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/msg/Editors.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config.msg; 2 | import java.util.Iterator; 3 | import org.tmotte.klonk.Editor; 4 | public interface Editors { 5 | public Editor getFirst(); 6 | public Iterable forEach(); 7 | public int size(); 8 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/msg/MainDisplay.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config.msg; 2 | import java.awt.Rectangle; 3 | import java.awt.Component; 4 | 5 | public interface MainDisplay { 6 | public boolean isMaximized(); 7 | public Rectangle getBounds(); 8 | public void setEditor(Component c); 9 | } 10 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/msg/Setter.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config.msg; 2 | 3 | /** 4 | * Nowadays this could obviously be replaced with java.util.function.Consumer. 5 | */ 6 | public interface Setter { 7 | public void set(T value); 8 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/msg/StatusUpdate.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config.msg; 2 | public interface StatusUpdate { 3 | public void show(String s); 4 | public void showBad(String s); 5 | public void showNoStatus(); 6 | public void showCapsLock(boolean b); 7 | public void showEncryption(boolean b); 8 | public void showRowColumn(int row, int column); 9 | public void showChangeThis(boolean b); 10 | public void showChangeAny(boolean b); 11 | public void showTitle(String title); 12 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/msg/UserNotify.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config.msg; 2 | import java.io.PrintWriter; 3 | import org.tmotte.klonk.io.KLog; 4 | 5 | public class UserNotify { 6 | 7 | //////////////////////// 8 | // PRIVATE VARIABLES: // 9 | //////////////////////// 10 | 11 | private KLog klog; 12 | private Setter alerter; 13 | private boolean ensureThreadSafeUI=false; 14 | private Setter allPurposeExceptionHandler= 15 | (Throwable t) ->alert(t); 16 | 17 | //////////////////////// 18 | // CONSTRUCTION & DI: // 19 | //////////////////////// 20 | 21 | public UserNotify(java.io.OutputStream out) { 22 | this(new KLog(out)); 23 | } 24 | public UserNotify(KLog klog) { 25 | this.klog=klog; 26 | } 27 | public UserNotify setUI(Setter alerter) { 28 | return setUI(alerter, false); 29 | } 30 | public UserNotify setUI(Setter alerter, boolean ensureThreadSafe) { 31 | this.alerter=alerter; 32 | this.ensureThreadSafeUI=ensureThreadSafe; 33 | return this; 34 | } 35 | public Setter getExceptionHandler(){ 36 | return allPurposeExceptionHandler; 37 | } 38 | 39 | ////////////// 40 | // LOGGING: // 41 | ////////////// 42 | 43 | public void log(String s){ 44 | klog.log(s); 45 | } 46 | public void log(Throwable e){ 47 | klog.log(e); 48 | } 49 | public void log(Throwable e, String s){ 50 | klog.log(e, s); 51 | } 52 | 53 | ////////////////// 54 | // POPUP-ALERT: // 55 | ////////////////// 56 | 57 | public void alert(final String s) { 58 | if (alerter==null) 59 | log("UserNotify: alerter is missing, message was: "+s); 60 | else 61 | if (ensureThreadSafeUI) 62 | javax.swing.SwingUtilities.invokeLater(new Runnable() { 63 | public void run() { 64 | alerter.set(s); 65 | } 66 | }); 67 | else 68 | alerter.set(s); 69 | } 70 | public void alert(Throwable t, final String s) { 71 | log(t); 72 | alert(s+" (see log for details) "+t); 73 | } 74 | public void alert(Throwable e) { 75 | log(e); 76 | alert("Internal error, see log for details: "+e.getMessage()); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/msg/UserServer.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config.msg; 2 | 3 | public class UserServer { 4 | public final String user; 5 | public final String server; 6 | public UserServer(String user, String server) { 7 | this.user=user; 8 | this.server=server; 9 | } 10 | public @Override boolean equals(Object o) { 11 | if (o instanceof UserServer) { 12 | UserServer us=(UserServer)o; 13 | return this.user.equals(us.user) && this.server.equals(us.server); 14 | } 15 | else 16 | return false; 17 | } 18 | public @Override int hashCode(){ 19 | return user.hashCode() + server.hashCode(); 20 | } 21 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/option/DelimiterOpts.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config.option; 2 | import java.util.regex.Pattern; 3 | import java.util.regex.Matcher; 4 | 5 | 6 | public class DelimiterOpts { 7 | 8 | 9 | public final static String 10 | LFs=new String(new char[]{10}), 11 | CRs=new String(new char[]{13}); 12 | public final static String 13 | CRLFs=CRs+LFs; 14 | public final static Pattern pattern=Pattern.compile("([\\r][\\n]|[\\r]|[\\n])"); 15 | 16 | 17 | public String defaultOption=CRLFs, 18 | thisFile=CRLFs; 19 | 20 | public static String detect(String s) { 21 | Matcher m=pattern.matcher(s); 22 | if (m.find()) 23 | return s.substring(m.start(), m.end()); 24 | return null; 25 | } 26 | public String toString() { 27 | return 28 | "Default: " +translateToReadable(defaultOption)+ 29 | "\nThis file: "+translateToReadable(thisFile); 30 | } 31 | 32 | //////////////// 33 | // TRANSLATE: // 34 | //////////////// 35 | 36 | public static String translateFromReadable(String s) { 37 | if (s==null) fail("Null input"); 38 | else 39 | if (s.equals("CR")) return CRs; 40 | else 41 | if (s.equals("CR-LF")) return CRLFs; 42 | else 43 | if (s.equals("LF")) return LFs; 44 | else fail("Invalid input: "+s); 45 | return null; 46 | } 47 | public static String translateToReadable(String s) { 48 | if (s==null) 49 | fail("Received null"); 50 | else 51 | if (s.equals(CRs)) return "CR"; 52 | else 53 | if (s.equals(CRLFs))return "CR-LF"; 54 | else 55 | if (s.equals(LFs)) return "LF"; 56 | else 57 | fail("Invalid translation from actual: "+s); 58 | return null; 59 | } 60 | 61 | ///////////// 62 | // ERRORS: // 63 | ///////////// 64 | 65 | private static void fail(String error) { 66 | throw new RuntimeException(error); 67 | } 68 | 69 | 70 | /////////// 71 | // TEST: // 72 | /////////// 73 | 74 | 75 | public static void main(String[] args) throws Exception { 76 | java.io.InputStreamReader br=new java.io.InputStreamReader(new java.io.FileInputStream(args[0])); 77 | int charsRead; 78 | char[] readBuffer=new char[224096]; 79 | while ((charsRead=br.read(readBuffer, 0, readBuffer.length))>0){ 80 | String s=new String(readBuffer, 0, charsRead); 81 | Matcher m=pattern.matcher(s); 82 | int i=0; 83 | while (m.find(i)) 84 | System.out.print( 85 | s.substring(i, m.start())+ 86 | translateFromReadable( 87 | translateToReadable( 88 | s.substring(m.start(), i=m.end()) 89 | ) 90 | ) 91 | ); 92 | 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/config/option/EncryptionOptions.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.config.option; 2 | public class EncryptionOptions { 3 | char[] pass={}; 4 | int bits; 5 | public @Override String toString() { 6 | return pass.length + " " + bits; 7 | } 8 | public void nullify() { 9 | for (int i=0; i> favoriteFileListener, favoriteDirListener; 17 | 18 | //Private data: 19 | private ArrayList favoriteDirs, favoriteFiles; 20 | 21 | public CtrlFavorites( 22 | KPersist persist, 23 | Setter> favoriteFileListener, 24 | Setter> favoriteDirListener 25 | ) { 26 | this.persist=persist; 27 | this.favoriteFileListener=favoriteFileListener; 28 | this.favoriteDirListener=favoriteDirListener; 29 | favoriteDirs =new ArrayList<>(KPersist.maxFavorite); 30 | favoriteFiles =new ArrayList<>(KPersist.maxFavorite); 31 | persist.getFavorites(favoriteFiles, favoriteDirs); 32 | favoriteFileListener.set(favoriteFiles); 33 | favoriteDirListener.set(favoriteDirs); 34 | } 35 | public Collection getFiles() { 36 | return favoriteFiles; 37 | } 38 | public Collection getDirs() { 39 | return favoriteDirs; 40 | } 41 | public void set() { 42 | favoriteFileListener.set(favoriteFiles); 43 | favoriteDirListener.set(favoriteDirs); 44 | persist.setFavoriteFiles(favoriteFiles); 45 | persist.setFavoriteDirs(favoriteDirs); 46 | persist.save(); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/controller/CtrlFileOther.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.controller; 2 | import org.tmotte.klonk.io.Printing; 3 | import org.tmotte.common.swang.CurrentOS; 4 | import org.tmotte.common.swang.SimpleClipboard; 5 | import org.tmotte.klonk.Editor; 6 | import org.tmotte.klonk.config.msg.StatusUpdate; 7 | import org.tmotte.klonk.config.msg.Editors; 8 | import java.util.LinkedList; 9 | 10 | 11 | public class CtrlFileOther { 12 | private Editors editors; 13 | private StatusUpdate statusBar; 14 | private CtrlFavorites ctrlFavorites; 15 | private CurrentOS currentOS; 16 | 17 | public CtrlFileOther( 18 | Editors editors, StatusUpdate statusBar, CtrlFavorites ctrlFavorites, CurrentOS currentOS 19 | ) { 20 | this.editors=editors; 21 | this.statusBar=statusBar; 22 | this.ctrlFavorites=ctrlFavorites; 23 | this.currentOS=currentOS; 24 | } 25 | 26 | public void doDocumentDirectoryExplore() { 27 | try{ 28 | String cmd=null; 29 | if (currentOS.isOSX) 30 | cmd="open"; 31 | else 32 | if (currentOS.isMSWindows) 33 | cmd="explorer"; 34 | else 35 | throw new RuntimeException("Unknown operating system"); 36 | String filename=editors.getFirst().getFile().getParentFile().getCanonicalPath(); 37 | Runtime.getRuntime().exec(new String[]{cmd, filename}); 38 | } catch (Exception e) { 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | 43 | public void doPrint() { 44 | if (Printing.print(editors.getFirst().getTextArea())) 45 | statusBar.show("Print job scheduled"); 46 | else 47 | statusBar.showBad("Action cancelled"); 48 | } 49 | 50 | public void doClipboardDoc() { 51 | toClipboard( 52 | ControllerUtils.getFullPath(editors.getFirst().getFile()) 53 | ); 54 | } 55 | public void doClipboardDocDir() { 56 | toClipboard( 57 | ControllerUtils.getFullPath(editors.getFirst().getFile().getParentFile()) 58 | ); 59 | } 60 | 61 | public void doAddCurrentToFaveDirs(){ 62 | String s=ControllerUtils.getFullPath(editors.getFirst().getFile().getParentFile()); 63 | ctrlFavorites.getDirs().add(s); 64 | ctrlFavorites.set(); 65 | statusBar.show("\""+s+"\" added to favorite directories."); 66 | } 67 | 68 | public void doAddCurrentToFaveFiles(){ 69 | String s=ControllerUtils.getFullPath(editors.getFirst().getFile()); 70 | ctrlFavorites.getFiles().add(s); 71 | ctrlFavorites.set(); 72 | statusBar.show("\""+s+"\" added to favorite files."); 73 | } 74 | 75 | 76 | 77 | private void toClipboard(String name) { 78 | SimpleClipboard.set(name); 79 | statusBar.show("Copied to clipboard: "+name); 80 | } 81 | 82 | public static void main(String[] args) throws Exception { 83 | java.io.File file=new java.io.File(args[0]); 84 | System.out.println(file.getCanonicalPath()); 85 | System.out.println(file.getCanonicalFile().getParentFile()); 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/controller/CtrlMarks.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.controller; 2 | import org.tmotte.klonk.Editor; 3 | import org.tmotte.klonk.config.msg.StatusUpdate; 4 | import org.tmotte.klonk.config.msg.Editors; 5 | import org.tmotte.klonk.config.msg.Setter; 6 | import org.tmotte.common.text.DelimitedString; 7 | import javax.swing.SwingUtilities; 8 | import java.util.LinkedList; 9 | 10 | public class CtrlMarks { 11 | private Editors editors; 12 | private StatusUpdate status; 13 | private Setter markStateListener; 14 | 15 | public CtrlMarks(Editors editors, StatusUpdate status, Setter markStateListener) { 16 | this.editors=editors; 17 | this.status=status; 18 | this.markStateListener=markStateListener; 19 | } 20 | 21 | 22 | public void doMarkSet() { 23 | Editor e=editors.getFirst(); 24 | int i=e.doSetMark(); 25 | if (i!=-1){ 26 | status.show("Mark set"); 27 | markStatus.go(i, e.getMarkCount(), true); 28 | } 29 | else 30 | status.showBad("Mark already set at this position"); 31 | if (i!=-1) 32 | markStateListener.set(true); 33 | } 34 | public void doMarkGoToPrevious() { 35 | Editor e=editors.getFirst(); 36 | int i=e.doMarkGoToPrevious(); 37 | if (i!=-1) 38 | markStatus.go(i, e.getMarkCount(), false); 39 | else 40 | status.showBad("Cursor is before first mark."); 41 | } 42 | public void doMarkGoToNext() { 43 | Editor e=editors.getFirst(); 44 | int i=e.doMarkGoToNext(); 45 | if (i!=-1) 46 | markStatus.go(i, e.getMarkCount(), false); 47 | else 48 | status.showBad("Cursor is after last mark."); 49 | } 50 | public void doMarkClearCurrent() { 51 | int i=editors.getFirst().doMarkClearCurrent(); 52 | if (i==-1) 53 | status.showBad("Cursor is not on a set mark."); 54 | else 55 | status.show("Mark cleared; "+i+" marks left."); 56 | if (i==0) 57 | markStateListener.set(false); 58 | } 59 | public void doMarkClearAll() { 60 | editors.getFirst().doClearMarks(); 61 | status.show("All marks cleared"); 62 | markStateListener.set(false); 63 | } 64 | 65 | // Had to create this because updates weren't showing up. Dunno. 66 | private MarkStatus markStatus=new MarkStatus(); 67 | private class MarkStatus implements Runnable { 68 | boolean set; int i, count; 69 | public void go(int i, int count, boolean set) { 70 | this.i=i; 71 | this.count=count; 72 | this.set=set; 73 | SwingUtilities.invokeLater(this); 74 | } 75 | public void run() { 76 | status.show("Mark "+i+" of "+count+(set ?" set." :".")); 77 | } 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/controller/CtrlOptions.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.controller; 2 | import java.util.LinkedList; 3 | import java.util.List; 4 | import org.tmotte.klonk.Editor; 5 | import org.tmotte.klonk.config.KPersist; 6 | import org.tmotte.klonk.config.msg.Editors; 7 | import org.tmotte.klonk.config.msg.Setter; 8 | import org.tmotte.klonk.config.msg.StatusUpdate; 9 | import org.tmotte.klonk.config.option.FontOptions; 10 | import org.tmotte.klonk.config.option.DelimiterOpts; 11 | import org.tmotte.klonk.config.option.SSHOptions; 12 | import org.tmotte.klonk.config.option.TabAndIndentOptions; 13 | import org.tmotte.klonk.ssh.SSHConnections; 14 | import org.tmotte.klonk.windows.popup.Favorites; 15 | import org.tmotte.klonk.windows.popup.FontPicker; 16 | import org.tmotte.klonk.windows.popup.LineDelimiterListener; 17 | import org.tmotte.klonk.windows.popup.LineDelimiters; 18 | import org.tmotte.klonk.windows.popup.TabsAndIndents; 19 | import org.tmotte.klonk.windows.popup.ssh.SSHOptionPicker; 20 | 21 | public class CtrlOptions { 22 | 23 | private Editors editors; 24 | private StatusUpdate statusBar; 25 | private KPersist persist; 26 | private CtrlFavorites ctrlFavorites; 27 | private TabAndIndentOptions taio; 28 | private FontOptions fontOptions; 29 | private SSHOptions sshOptions; 30 | private LineDelimiters lineDelimiters; 31 | private SSHConnections sshConns; 32 | 33 | private Favorites favorites; 34 | private SSHOptionPicker sshOptionPicker; 35 | private TabsAndIndents tabsAndIndents; 36 | private FontPicker fontPicker; 37 | 38 | private LineDelimiterListener delimListener; 39 | private List> fontListeners; 40 | private List> tabIndentOptionListeners; 41 | 42 | public CtrlOptions( 43 | Editors editors, StatusUpdate statusBar, KPersist persist, CtrlFavorites ctrlFavorites, 44 | LineDelimiterListener delimListener, 45 | List> fontListeners, 46 | List> tabIndentOptionListeners, 47 | SSHConnections sshConns, 48 | SSHOptionPicker sshOptionPicker, TabsAndIndents tabsAndIndents, 49 | Favorites favorites, FontPicker fontPicker, LineDelimiters lineDelimiters 50 | ) { 51 | this.editors=editors; 52 | this.statusBar=statusBar; 53 | this.persist=persist; 54 | this.ctrlFavorites=ctrlFavorites; 55 | this.delimListener=delimListener; 56 | this.fontListeners=fontListeners; 57 | this.tabIndentOptionListeners=tabIndentOptionListeners; 58 | this.sshConns=sshConns; 59 | 60 | this.sshOptionPicker=sshOptionPicker; 61 | this.tabsAndIndents=tabsAndIndents; 62 | this.favorites=favorites; 63 | this.fontPicker=fontPicker; 64 | this.lineDelimiters=lineDelimiters; 65 | 66 | this.taio=persist.getTabAndIndentOptions(); 67 | this.fontOptions=persist.getFontAndColors(); 68 | this.sshOptions=persist.getSSHOptions(); 69 | } 70 | 71 | public void doWordWrap() { 72 | boolean b=persist.getWordWrap(); 73 | persist.setWordWrap(!b); 74 | persist.save(); 75 | for (Editor e: editors.forEach()) 76 | e.setWordWrap(!b); 77 | } 78 | 79 | public void doAutoTrim() { 80 | boolean b=persist.getAutoTrim(); 81 | persist.setAutoTrim(!b); 82 | persist.save(); 83 | for (Editor e: editors.forEach()) 84 | e.setAutoTrim(!b); 85 | } 86 | 87 | 88 | public void doTabsAndIndents(){ 89 | taio.indentionMode=editors.getFirst().getTabsOrSpaces(); 90 | if (tabsAndIndents.show(taio)){ 91 | editors.getFirst().setTabsOrSpaces(taio.indentionMode); 92 | for (Editor e: editors.forEach()) 93 | e.setTabAndIndentOptions(taio); 94 | for (Setter setter: tabIndentOptionListeners) 95 | setter.set(taio); 96 | persist.setTabAndIndentOptions(taio); 97 | persist.save(); 98 | statusBar.show("Changes to tabs & indents saved"); 99 | } 100 | else 101 | statusBar.showBad("Changes to tabs & indents cancelled"); 102 | } 103 | 104 | public void doFontBigger() { 105 | fontOptions.setFontSize(fontOptions.getFontSize()+1); 106 | pushFont(); 107 | } 108 | public Runnable getFontBiggerLambda() { 109 | return ()->doFontBigger(); 110 | } 111 | public void doFontSmaller() { 112 | int i=fontOptions.getFontSize()-1; 113 | if (i>0) { 114 | fontOptions.setFontSize(i); 115 | pushFont(); 116 | } 117 | else 118 | statusBar.showBad("Cannot make font any smaller"); 119 | } 120 | public Runnable getFontSmallerLambda() { 121 | return ()->doFontSmaller(); 122 | } 123 | public void doFontAndColors() { 124 | if (!fontPicker.show(fontOptions)){ 125 | statusBar.showBad("Changes to font & colors cancelled"); 126 | return; 127 | } 128 | statusBar.show("Changes to font & colors saved"); 129 | pushFont(); 130 | } 131 | private void pushFont() { 132 | for (Editor e: editors.forEach()) 133 | e.setFont(fontOptions); 134 | for (Setter setter: fontListeners) 135 | setter.set(fontOptions); 136 | persist.setFontAndColors(fontOptions); 137 | persist.save(); 138 | } 139 | 140 | public void doFavorites() { 141 | if (!favorites.show(ctrlFavorites.getFiles(), ctrlFavorites.getDirs())) 142 | statusBar.showBad("Changes to favorite files/directories cancelled"); 143 | else { 144 | ctrlFavorites.set(); 145 | statusBar.show("Changes to favorite files/directories saved"); 146 | } 147 | } 148 | 149 | public void doLineDelimiters(){ 150 | DelimiterOpts k=new DelimiterOpts(); 151 | k.defaultOption=persist.getDefaultLineDelimiter(); 152 | k.thisFile=editors.getFirst().getLineBreaker(); 153 | lineDelimiters.show(k, delimListener); 154 | } 155 | 156 | public void doSSH(){ 157 | List hosts=sshConns.getConnectedHosts(); 158 | if (sshOptionPicker.show(sshOptions, hosts)){ 159 | persist.writeSSHOptions(); 160 | persist.save(); 161 | for (String dis: hosts) 162 | sshConns.close(dis); 163 | sshConns.withOptions(sshOptions); 164 | statusBar.show("SSH changes saved"); 165 | } 166 | else 167 | statusBar.showBad("Changes to SSH cancelled"); 168 | 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/controller/CtrlOther.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.controller; 2 | import org.tmotte.klonk.Editor; 3 | import org.tmotte.klonk.config.msg.StatusUpdate; 4 | import org.tmotte.klonk.config.msg.Editors; 5 | import org.tmotte.klonk.windows.popup.Shell; 6 | import org.tmotte.klonk.windows.popup.Help; 7 | import org.tmotte.klonk.windows.popup.About; 8 | import java.util.LinkedList; 9 | 10 | 11 | public class CtrlOther { 12 | private Shell shell; 13 | private Help help; 14 | private About about; 15 | 16 | public CtrlOther(Shell shell, Help help, About about) { 17 | this.shell=shell; 18 | this.help=help; 19 | this.about=about; 20 | } 21 | 22 | public void doShell() { 23 | shell.show(); 24 | } 25 | public void doHelpShortcuts() { 26 | help.show(); 27 | } 28 | public void doHelpAbout() { 29 | about.show(); 30 | } 31 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/controller/CtrlSearch.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.controller; 2 | import org.tmotte.klonk.Editor; 3 | import org.tmotte.klonk.config.msg.Editors; 4 | import org.tmotte.klonk.config.msg.StatusUpdate; 5 | import org.tmotte.klonk.edit.MyTextArea; 6 | import org.tmotte.klonk.windows.popup.GoToLine; 7 | import org.tmotte.klonk.windows.popup.FindAndReplace; 8 | 9 | public class CtrlSearch { 10 | 11 | private Editors editors; 12 | private FindAndReplace findAndReplace; 13 | private GoToLine gtl; 14 | private StatusUpdate statusBar; 15 | 16 | public CtrlSearch(Editors editors, StatusUpdate statusBar, FindAndReplace findAndReplace, GoToLine gtl) { 17 | this.statusBar=statusBar; 18 | this.editors=editors; 19 | this.findAndReplace=findAndReplace; 20 | this.gtl=gtl; 21 | } 22 | 23 | 24 | public void doSearchFind(){ 25 | findAndReplace.doFind(editors.getFirst().getTextArea()); 26 | } 27 | public void doSearchReplace(){ 28 | findAndReplace.doReplace(editors.getFirst().getTextArea()); 29 | } 30 | public void doSearchRepeat(){ 31 | findAndReplace.repeatFindReplace(editors.getFirst().getTextArea(), true); 32 | } 33 | public void doSearchRepeatBackwards(){ 34 | findAndReplace.repeatFindReplace(editors.getFirst().getTextArea(), false); 35 | } 36 | public void doSearchGoToLine() { 37 | MyTextArea target=editors.getFirst().getTextArea(); 38 | int i=gtl.show(); 39 | if (i==-1) 40 | statusBar.showBad("Go to line cancelled."); 41 | else 42 | if (!target.goToLine(i-1)) 43 | statusBar.showBad("Line number "+i+" is out of range"); 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/controller/CtrlSelection.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.controller; 2 | import java.util.LinkedList; 3 | import org.tmotte.common.swang.SimpleClipboard; 4 | import org.tmotte.common.text.DelimitedString; 5 | import org.tmotte.klonk.Editor; 6 | import org.tmotte.klonk.config.msg.Editors; 7 | import org.tmotte.klonk.config.msg.StatusUpdate; 8 | import org.tmotte.klonk.config.msg.Setter; 9 | 10 | public class CtrlSelection { 11 | private Editors editors; 12 | private StatusUpdate status; 13 | private Setter alerter; 14 | 15 | public CtrlSelection(Editors editors, StatusUpdate status, Setter alerter) { 16 | this.editors=editors; 17 | this.status=status; 18 | this.alerter=alerter; 19 | } 20 | 21 | public void doWeirdUpperCase() { 22 | editors.getFirst().doUpperCase(); 23 | } 24 | public void doWeirdLowerCase() { 25 | editors.getFirst().doLowerCase(); 26 | } 27 | public void doWeirdSortLines() { 28 | if (!editors.getFirst().doSortLines()) 29 | status.show("Only one line was selected for sort"); 30 | } 31 | public void doWeirdSelectionSize() { 32 | alerter.set("Selected text size: "+editors.getFirst().getSelection().length()); 33 | } 34 | public void doWeirdAsciiValues() { 35 | String text=editors.getFirst().getSelection(); 36 | DelimitedString result=new DelimitedString(" "); 37 | int len=text.length(); 38 | for (int i=0; i1000) 43 | r=r.substring(1000)+"..."; 44 | alerter.set("ASCII/UTF-8 values copied to Clipboard as: "+r); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/controller/CtrlUndo.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.controller; 2 | import java.util.List; 3 | import org.tmotte.klonk.Editor; 4 | import org.tmotte.klonk.config.KPersist; 5 | import org.tmotte.klonk.config.msg.Editors; 6 | import org.tmotte.klonk.config.msg.Setter; 7 | import org.tmotte.klonk.config.msg.StatusUpdate; 8 | import org.tmotte.klonk.windows.popup.YesNoCancel; 9 | 10 | 11 | public class CtrlUndo { 12 | private Editors editors; 13 | private StatusUpdate status; 14 | private KPersist persist; 15 | private boolean fastUndos=false; 16 | private YesNoCancel yesNo; 17 | private List> fastUndoListeners; 18 | 19 | public CtrlUndo(Editors editors, List> fastUndoListeners, StatusUpdate status, YesNoCancel yesNo, KPersist persist) { 20 | this.editors=editors; 21 | this.yesNo=yesNo; 22 | this.status=status; 23 | this.persist=persist; 24 | this.fastUndoListeners=fastUndoListeners; 25 | fastUndos=persist.getFastUndos(); 26 | } 27 | 28 | public void doUndo(){ 29 | editors.getFirst().undo(); 30 | } 31 | public void doRedo(){ 32 | editors.getFirst().redo(); 33 | } 34 | public void doUndoToBeginning() { 35 | editors.getFirst().undoToBeginning(); 36 | status.show("Undone to beginning"); 37 | } 38 | public void doRedoToEnd() { 39 | editors.getFirst().redoToEnd(); 40 | status.show("Redone to end"); 41 | } 42 | public void undoToHistorySwitch() { 43 | if (editors.getFirst().undoToHistorySwitch()) 44 | status.show("Undone to previous history rewrite"); 45 | else 46 | status.showBad("No history rewrites found"); 47 | } 48 | public void redoToHistorySwitch() { 49 | if (editors.getFirst().redoToHistorySwitch()) 50 | status.show("Redone to next history rewrite"); 51 | else 52 | status.showBad("No history rewrites found"); 53 | } 54 | public void doUndoFast() { 55 | persist.setFastUndos(fastUndos=!fastUndos); 56 | persist.save(); 57 | for (Editor e: editors.forEach()) 58 | e.setFastUndos(fastUndos); 59 | fastUndoListeners.stream().forEach(f -> f.set(fastUndos)); 60 | } 61 | public void doClearUndos() { 62 | if (yesNo.show("Clear undos?").isYes()){ 63 | editors.getFirst().clearUndos(); 64 | status.show("Undo stack cleared"); 65 | } 66 | else 67 | status.showBad("Action cancelled"); 68 | } 69 | public void doClearRedos() { 70 | if (yesNo.show("Clear redos?").isYes()){ 71 | editors.getFirst().clearRedos(); 72 | status.show("Redo stack cleared"); 73 | } 74 | else 75 | status.showBad("Action cancelled"); 76 | } 77 | public void doClearUndosAndRedos() { 78 | if (yesNo.show("Clear undos and redos?").isYes()){ 79 | editors.getFirst().clearUndos(); 80 | editors.getFirst().clearRedos(); 81 | status.show("Undos & redos cleared"); 82 | } 83 | else 84 | status.showBad("Action cancelled"); 85 | } 86 | 87 | 88 | 89 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/controller/Recents.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.controller; 2 | import java.io.File; 3 | import java.util.ArrayList; 4 | import java.util.LinkedList; 5 | import java.util.List; 6 | import org.tmotte.klonk.Editor; 7 | import org.tmotte.klonk.config.KPersist; 8 | import org.tmotte.klonk.config.msg.Setter; 9 | import org.tmotte.klonk.config.msg.UserServer; 10 | import org.tmotte.klonk.ssh.SSHFile; 11 | 12 | /** This is a secondary controller; it is invoked by other controllers. */ 13 | class Recents { 14 | 15 | //DI resources: 16 | private KPersist persist; 17 | private Setter> recentFileListener, recentDirListener; 18 | 19 | //Private data: 20 | private ArrayList recentDirs, recentFiles; 21 | private ArrayList recentSSHConns; 22 | 23 | //Initialize: 24 | Recents(KPersist persist) { 25 | this.persist=persist; 26 | recentDirs =new ArrayList<>(KPersist.maxRecent); 27 | recentFiles =new ArrayList<>(KPersist.maxRecent); 28 | recentSSHConns=new ArrayList<>(KPersist.maxRecent); 29 | persist.getRecent(recentFiles, recentDirs); 30 | persist.getRecentSSH(recentSSHConns); 31 | } 32 | void setFileListener(Setter> recentFileListener) { 33 | this.recentFileListener=recentFileListener; 34 | recentFileListener.set(recentFiles); 35 | } 36 | void setDirListener(Setter> recentDirListener) { 37 | this.recentDirListener=recentDirListener; 38 | recentDirListener.set(recentDirs); 39 | } 40 | 41 | //Data retrieval: 42 | String getFirstDir() {return recentDirs.get(0);} 43 | boolean hasDirs() {return recentDirs.size()>0;} 44 | List getRecentSSHConns(){ 45 | return recentSSHConns; 46 | } 47 | 48 | //Events: 49 | void recentFileSavedNew(File file, File oldFile) { 50 | if (oldFile!=null && !oldFile.equals(file) && oldFile.exists()) 51 | //This is when we save-as and thus discard an existing file. 52 | recentFileClosed(oldFile); 53 | recentFileRemoveDirAdd(file); 54 | } 55 | void recentFileLoaded(File file) { 56 | recentFileRemoveDirAdd(file); 57 | } 58 | void recentFileClosed(File file){ 59 | String path=ControllerUtils.getFullPath(file); 60 | for (int i=recentFiles.size()-1; i>=0; i--) 61 | if (recentFiles.get(i).equals(path)) 62 | recentFiles.remove(i); 63 | recentFiles.add(0, path); 64 | if (recentFiles.size()>KPersist.maxRecent) 65 | recentFiles.remove(recentFiles.size()-1); 66 | persist.setRecentFiles(recentFiles); 67 | recentFileListener.set(recentFiles); 68 | File f=file.getParentFile(); 69 | addToRecentDirectories(file); 70 | } 71 | 72 | //////////////// 73 | // INTERNALS: // 74 | //////////////// 75 | 76 | private void recentFileRemoveDirAdd(File file) { 77 | int i=recentFiles.indexOf(ControllerUtils.getFullPath(file)); 78 | if (i>-1) { 79 | recentFiles.remove(i); 80 | recentFileListener.set(recentFiles); 81 | persist.setRecentFiles(recentFiles); 82 | } 83 | addToRecentDirectories(file); 84 | } 85 | 86 | private void addToRecentDirectories(File file) { 87 | //Directory: 88 | { 89 | File parent=file.getParentFile(); 90 | if (parent==null) 91 | return; 92 | String path=ControllerUtils.getFullPath(parent); 93 | int i=recentDirs.indexOf(path); 94 | if (i>-1) recentDirs.remove(i); 95 | else 96 | if (recentDirs.size()>KPersist.maxRecent) 97 | recentDirs.remove(recentDirs.size()-1); 98 | recentDirs.add(0, path); 99 | recentDirListener.set(recentDirs); 100 | persist.setRecentDirs(recentDirs); 101 | } 102 | 103 | //SSH host/server: 104 | { 105 | SSHFile ssf=SSHFile.cast(file); 106 | if (ssf!=null){ 107 | String user=ssf.getSSH().getUser(), 108 | host=ssf.getSSH().getHost(); 109 | if (recentSSHConns==null) 110 | recentSSHConns=new ArrayList(KPersist.maxRecent); 111 | boolean isFirst=false; 112 | for (int i=0; iKPersist.maxRecent) 123 | recentSSHConns.remove(recentSSHConns.size()-1); 124 | persist.setRecentSSH(recentSSHConns); 125 | } 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/edit/Indenter.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.edit; 2 | public class Indenter { 3 | 4 | // Config: 5 | boolean tabIndents=false; 6 | int tabSize=1; 7 | int spaceIndentLen=1; 8 | 9 | // State: 10 | private int startLen; 11 | 12 | // Externally used state: 13 | public StringBuilder buffer; 14 | public boolean blank=true; 15 | public boolean anyChange=false; 16 | public int endPos; 17 | public int pastBlock; 18 | public int lenChange; 19 | 20 | 21 | public void repair(StringBuilder lineStr) { 22 | init(lineStr); 23 | } 24 | 25 | public void init(String lineStr) { 26 | init(new StringBuilder(lineStr)); 27 | } 28 | 29 | public void init(StringBuilder buffer) { 30 | this.buffer=buffer; 31 | startLen=buffer.length(); 32 | blank=false; 33 | anyChange=false; 34 | endPos=-1; 35 | pastBlock=0; 36 | lenChange=0; 37 | 38 | final int blockSize=tabIndents ?tabSize :spaceIndentLen; 39 | for (int i=0; i 0) { 75 | if (remove) 76 | trimPastBlock(); 77 | else { 78 | final int blockSize=tabIndents ?tabSize :spaceIndentLen; 79 | if (tabIndents) { 80 | trimPastBlock(); 81 | buffer.insert(endPos++, '\t'); 82 | } 83 | else 84 | while (pastBlock++ < blockSize) 85 | buffer.insert(endPos++, ' '); 86 | } 87 | anyChange=true; 88 | } 89 | else 90 | if (remove) { 91 | if (buffer.length()>0) { 92 | if (tabIndents) { 93 | if (buffer.charAt(0)==' ') 94 | deleteFirstChars(tabSize); 95 | else 96 | if (buffer.charAt(0)=='\t') 97 | deleteFirstChar(); 98 | } 99 | else 100 | if (buffer.charAt(0)=='\t') 101 | deleteFirstChar(); 102 | else 103 | if (buffer.charAt(0)==' ') 104 | deleteFirstChars(spaceIndentLen); 105 | } 106 | } 107 | else 108 | if (tabIndents) 109 | insertFirstChar('\t'); 110 | else 111 | for (int i=0; i").append(indented).append("<-").append("\n"); 43 | if (anyChange==indented.equals(has)) 44 | throw new RuntimeException("Change mismatch: "+anyChange); 45 | if (!indented.equals(expects)) 46 | throw new RuntimeException("Mismatch"); 47 | } 48 | 49 | 50 | private void testRepair() { 51 | tabIndents=false; 52 | tabSize=2; 53 | spaceIndentLen=2; 54 | testRepair("_TT_", "_____"); 55 | testRepair("T__T", "______"); 56 | testRepair("_T", "__"); 57 | 58 | tabIndents=false; 59 | tabSize=4; 60 | spaceIndentLen=2; 61 | testRepair("_TT_", "____----_"); 62 | testRepair("T___", "____---"); 63 | 64 | if (true) return; 65 | 66 | tabIndents=false; 67 | tabSize=4; 68 | spaceIndentLen=4; 69 | testRepair("__T", "____", 4); 70 | testRepair("____T", "____T", 8); 71 | testRepair("T___", "T___", 7); 72 | 73 | tabIndents=true; 74 | tabSize=4; 75 | spaceIndentLen=2; 76 | testRepair("__T", "T", 4); 77 | testRepair("_T", "T", 4); 78 | testRepair("_T_T", "TT", 8); 79 | testRepair("____T", "____T", 8); 80 | testRepair("T___", "T___", 7); 81 | } 82 | 83 | private void testRepair(String has, String expects) { 84 | testRepair(has, expects, -1); 85 | } 86 | 87 | private void testRepair(String has, String expects, int viewLenExpect) { 88 | System.out.append("--------------\n") 89 | .append("Had: ").append(has).append("\n") 90 | .append("Want: ").append(expects).append("\n"); 91 | has=toIndents(has); 92 | expects=toIndents(expects); 93 | init(has); 94 | String repaired=buffer.toString(); 95 | System.out.append("Got:->").append(repaired).append("<-").append("\n"); 96 | if (anyChange==repaired.equals(has)) 97 | throw new RuntimeException("Change mismatch: "+anyChange); 98 | if (!repaired.equals(expects)) 99 | throw new RuntimeException("Mismatch"); 100 | } 101 | 102 | private static String toIndents(String s) { 103 | StringBuilder sb=new StringBuilder(); 104 | int len=s.length(); 105 | for (int i=0; i 4 ?0 :w/2; 27 | return this; 28 | } 29 | protected @Override synchronized void damage(Rectangle r) { 30 | if (r==null) 31 | return; 32 | 33 | // Give values to x,y,width,height (inherited from java.awt.Rectangle) 34 | x=r.x-halfWidth; 35 | y=r.y; 36 | height=r.height; 37 | 38 | // A value for width was probably set by paint(), which we leave alone. 39 | // But the first call to damage() precedes the first call to paint(), so 40 | // in this case we must be prepared to set a valid width, or else 41 | // paint() will receive a bogus clip area and caret will not get drawn properly. 42 | //if (width <=0) 43 | // width=getComponent().getWidth(); 44 | 45 | repaint(); // calls getComponent().repaint(x, y, width, height) 46 | } 47 | 48 | public @Override void paint(Graphics g) { 49 | JTextComponent comp=getComponent(); 50 | if (comp==null) 51 | return; 52 | 53 | Rectangle r=null; 54 | try { 55 | Rectangle2D r2=comp.modelToView2D(getDot()); 56 | if (r2==null) 57 | return; 58 | r=new Rectangle( 59 | (int)Math.round(r2.getX()), 60 | (int)Math.round(r2.getY()), 61 | (int)Math.round(r2.getWidth()), 62 | (int)Math.round(r2.getHeight()) 63 | ); 64 | } catch (BadLocationException e) { 65 | return; 66 | } 67 | 68 | if (x!=r.x || y!=r.y) { 69 | // paint() has been called directly, without a previous call to 70 | // damage(), so do some cleanup. (This happens, for example, when 71 | // the text component is resized.) 72 | repaint(); // erase previous location of caret 73 | x=r.x-halfWidth; // Update dimensions (width gets set later in this method) 74 | y=r.y; 75 | height=r.height; 76 | } 77 | 78 | g.setColor(comp.getCaretColor()); 79 | g.setXORMode(comp.getBackground()); // do this to draw in XOR mode 80 | 81 | width=betterWidth; 82 | if (isVisible()) 83 | g.fillRect(r.x-halfWidth, r.y, width, r.height); 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/edit/Spaceable.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.edit; 2 | 3 | /** Not threadsafe, as it doesn't need to be. */ 4 | public class Spaceable { 5 | 6 | final static char LINEFEED=((char)10), 7 | TAB=((char)9), 8 | SPACE=' '; 9 | final static String 10 | strRegular="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 11 | ,strWhite =new String(new char[]{TAB,SPACE}) 12 | ; 13 | final static String 14 | strRegularWhite=strRegular+strWhite; 15 | 16 | 17 | private static String possible; 18 | private static int len; 19 | private static void init(String p){ 20 | possible=p; 21 | len=possible.length(); 22 | } 23 | 24 | 25 | ////////////////// 26 | // // 27 | // GET RIGHT: // 28 | // // 29 | ////////////////// 30 | 31 | 32 | public static int getRight(String possible) { 33 | init(possible); 34 | return getRight(0); 35 | } 36 | private static int getRight(int startWith) { 37 | 38 | //If we're at the next to last character, done, use it: 39 | if (startWith>=len-1) return len; 40 | 41 | //Get first char and flip thru: 42 | char firstChar=possible.charAt(startWith++); 43 | if (strWhite.indexOf(firstChar)>-1) 44 | return rightJump(startWith, strWhite); 45 | else 46 | if (strRegular.indexOf(firstChar)>-1) 47 | return rightJump(startWith, strRegular); 48 | else 49 | return rightJumpSpecialChars(startWith); 50 | } 51 | private static int rightJump(int startWith, String accept) { 52 | for (int i=startWith; i-1; i--) 95 | if (toSearch.indexOf(possible.charAt(i))==-1) 96 | return i+1; 97 | return 0; 98 | } 99 | private static int leftJumpSpecialChars(int startWith) { 100 | for (int i=startWith; i>-1; i--){ 101 | char ch=possible.charAt(i); 102 | if (strRegularWhite.indexOf(ch)!=-1) 103 | return i+1; 104 | } 105 | return 0; 106 | } 107 | 108 | 109 | 110 | ////////////////// 111 | // // 112 | // OTHER STUFF: // 113 | // // 114 | ////////////////// 115 | 116 | 117 | 118 | public static void main(String[] args) throws Exception { 119 | if (args.length==0 || args[1].toLowerCase().contains("-help")){ 120 | System.out.println( 121 | "Usage: java org.tmotte.klonk.edit.Spaceable <-right|-left> " 122 | +"\n str will be searched for the next spot, from right or left, with backwards on or off. " 123 | ); 124 | return; 125 | } 126 | 127 | //Left or right or what? 128 | boolean right=false; 129 | if (args[0].equals("-right")) right=true; 130 | else 131 | if (args[0].equals("-left")) right=false; 132 | else{ 133 | System.err.println("Don't know what to do with: "+args[0]); 134 | return; 135 | } 136 | 137 | //What is the string 138 | String s=args[args.length-1]; 139 | 140 | //Now do the dirty work: 141 | if (right) testRight(s); 142 | else testLeft(s); 143 | System.out.flush(); 144 | } 145 | 146 | public static void testRight(String s) { 147 | int r=getRight(s); 148 | System.out.println(""+r+">"+s.substring(0,r)+"<"); 149 | } 150 | public static void testLeft(String s) { 151 | int l=getLeft(s); 152 | System.out.println(""+l+" "+s.length()+">"+s.substring(l,s.length())+"<"); 153 | } 154 | 155 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/edit/Undo.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.edit; 2 | import java.util.*; 3 | 4 | 5 | final class Undo { 6 | 7 | private ArrayDeque undos=new ArrayDeque(), 8 | redos=new ArrayDeque(); 9 | private UndoStep savedState; 10 | 11 | public Undo() { 12 | markSave(); 13 | } 14 | 15 | public void doAdd(int start, int len, String text, boolean doubleUp) { 16 | makeRedosUndos(); 17 | undos.add(new UndoStep(UndoStep.ADD, start, len, text, doubleUp, doubleUp)); 18 | } 19 | public void doRemove(int start, int len, String text, boolean doubleUp) { 20 | makeRedosUndos(); 21 | undos.add(new UndoStep(UndoStep.REMOVE, start, len, text, doubleUp, doubleUp)); 22 | } 23 | public void markSave() { 24 | if (savedState!=null) 25 | //This will only be null when we invoke from the constructor. 26 | //This object is marked as "limbo" to indicate it means 27 | //nothing and is awaiting cleanup, which happens in getLast(): 28 | savedState.uType=UndoStep.LIMBO; 29 | savedState=UndoStep.createSaveState(); 30 | undos.add(savedState); 31 | } 32 | 33 | public boolean isSavedState() { 34 | //Save state can be on either stack, doesn't matter. 35 | //If it's on either, we're at the saved state. 36 | UndoStep us1=undos.size()==0 ?null :undos.getLast(), 37 | us2=redos.size()==0 ?null :redos.getLast(); 38 | return (us1!=null && us1.uType==UndoStep.MARK_SAVE) 39 | || 40 | (us2!=null && us2.uType==UndoStep.MARK_SAVE) 41 | ; 42 | } 43 | public void debug(String yeah) { 44 | StringBuilder sb=new StringBuilder(yeah); 45 | for (UndoStep us: undos) 46 | us.debug("", sb); 47 | sb.append(" ---- "); 48 | for (UndoStep us: redos) 49 | us.debug("", sb); 50 | System.out.println(sb.toString()); 51 | System.out.flush(); 52 | } 53 | 54 | /////////// 55 | // UNDO: // 56 | /////////// 57 | 58 | public boolean hasHistorySwitchUndo() { 59 | return hasHistorySwitch(undos); 60 | } 61 | public boolean hasHistorySwitchRedo() { 62 | return hasHistorySwitch(redos); 63 | } 64 | public UndoStep doUndo() { 65 | return removeLast(undos, redos); 66 | } 67 | public UndoStep getUndo() { 68 | return getLast(undos, redos); 69 | } 70 | public boolean hasUndos() { 71 | return getLast(undos, redos)!=null; 72 | } 73 | public void clearUndos() { 74 | undos.clear(); 75 | } 76 | 77 | 78 | /////////// 79 | // REDO: // 80 | /////////// 81 | 82 | public UndoStep doRedo() { 83 | return removeLast(redos, undos); 84 | } 85 | public UndoStep getRedo() { 86 | return getLast(redos, undos); 87 | } 88 | public boolean hasRedos() { 89 | return getLast(redos, undos)!=null; 90 | } 91 | public void clearRedos() { 92 | redos.clear(); 93 | } 94 | 95 | //////////////// 96 | // INTERNALS: // 97 | //////////////// 98 | 99 | private static boolean hasHistorySwitch(ArrayDeque deq) { 100 | for (UndoStep step: deq) 101 | if (step.doubleUp) 102 | return true; 103 | return false; 104 | } 105 | 106 | private static UndoStep removeLast(ArrayDeque mainList, ArrayDeque otherList) { 107 | if (mainList.size()==0) 108 | return null; 109 | UndoStep st=mainList.removeLast(); 110 | if (st.uType!=UndoStep.LIMBO) 111 | otherList.add(st); 112 | return st.isAddOrRemove() ?st :removeLast(mainList, otherList); 113 | } 114 | 115 | private UndoStep getLast(ArrayDeque mainList, ArrayDeque otherList) { 116 | 117 | //Normal undos: 118 | if (mainList.size()==0) 119 | return null; 120 | UndoStep us=mainList.getLast(); 121 | if (us.isAddOrRemove()) 122 | return us; 123 | 124 | //SAVE_STATE is pushed; skip LIMBO (by default). 125 | //Then recurse: 126 | mainList.removeLast(); 127 | if (us.isSaveState()) 128 | otherList.add(us); 129 | return getLast(mainList, otherList); 130 | } 131 | 132 | private void makeRedosUndos() { 133 | //This is seriously major thinking backwards. 134 | //1. Push the original edits onto the redo stack, first to last 135 | //2. Push their undos onto the redo stack, last to first: 136 | for (Iterator iter=redos.descendingIterator(); iter.hasNext();){ 137 | UndoStep us=iter.next(); 138 | undos.add(us); 139 | } 140 | UndoStep last=null, first=redos.size()==0 ?null :redos.getFirst(); 141 | if (first!=null) 142 | first.doubleUpDone=true; 143 | for (Iterator iter=redos.iterator(); iter.hasNext();){ 144 | last=iter.next(); 145 | int uType; 146 | if (last.isAddOrRemove()){ 147 | last=new UndoStep( 148 | last.uType==UndoStep.REMOVE ?UndoStep.ADD :UndoStep.REMOVE 149 | ,last.start, last.len, last.text, true, false //last.doubleUp 150 | ); 151 | undos.add(last);//Save state already added once 152 | } 153 | } 154 | if (last!=null) 155 | last.doubleUpDone=true; 156 | redos.clear(); 157 | } 158 | 159 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/edit/UndoEvent.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.edit; 2 | 3 | public class UndoEvent { 4 | public boolean isNoMoreUndosError=false, 5 | isNoMoreRedosError=false, 6 | isUndoSaveStable=false; 7 | UndoEvent setNoMoreUndosError() { 8 | isNoMoreUndosError=true; 9 | return this; 10 | } 11 | UndoEvent setNoMoreRedosError() { 12 | isNoMoreRedosError=true; 13 | return this; 14 | } 15 | UndoEvent setUndoSaveStable() { 16 | isUndoSaveStable=true; 17 | return this; 18 | } 19 | public String toString(){ 20 | return "No more undos: "+isNoMoreUndosError+ 21 | " No more redos: "+isNoMoreRedosError 22 | +" Stable: "+isUndoSaveStable; 23 | } 24 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/edit/UndoListener.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.edit; 2 | public interface UndoListener { 3 | public void happened(UndoEvent mta); 4 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/edit/UndoSimilar.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.edit; 2 | public class UndoSimilar { 3 | 4 | public static boolean matches(String ass, String bss) { 5 | if (ass==null || bss==null || ass.length()==0 || bss.length()==0) 6 | return false; 7 | char a=ass.charAt(0), b=bss.charAt(0); 8 | if (a==b) return true; 9 | final String[] possible={ 10 | Selectable.strLetterCaps+Selectable.strLetterLow 11 | ,Selectable.strNum+"/-.+*" 12 | //,"<{[(\">}])" 13 | ,Selectable.strWhite 14 | }; 15 | return matches(a, b, possible); 16 | } 17 | private static boolean matches(char a, char b, final String[] maybe) { 18 | for (String s: maybe) { 19 | int r=matches(a, b, s); 20 | //Stop if we find out one matches and the other doesn't; 21 | //or if we find out they both match. 22 | if (r==2) 23 | return false; 24 | else 25 | if (r==1) 26 | return true; 27 | } 28 | return false; 29 | } 30 | private static int matches(char a, char b, String maybe) { 31 | boolean ba=maybe.indexOf(a)!=-1, 32 | bb=maybe.indexOf(b)!=-1; 33 | if (!ba && !bb) return 0; 34 | else 35 | if (ba && bb) return 1; 36 | else return 2; 37 | } 38 | 39 | public static void main(String[] args) throws Exception { 40 | String[] maybe={"abcdef", "ABCDEF", "12345", ")(*&^"}; 41 | char a1=(char)System.in.read(), a2=(char)System.in.read(); 42 | System.out.println(matches(""+a1, ""+a2)); 43 | } 44 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/edit/UndoStep.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.edit; 2 | 3 | public final class UndoStep { 4 | public final static int ADD=1, REMOVE=2, MARK_SAVE=3, LIMBO=4; 5 | 6 | public int start, len, uType; 7 | public String text; 8 | public boolean doubleUp, doubleUpDone; 9 | UndoStep(int uType, int start, int len, String text, boolean doubleUp) { 10 | this(uType, start, len, text, doubleUp, false); 11 | } 12 | UndoStep(int uType, int start, int len, String text, boolean doubleUp, boolean doubleUpDone) { 13 | this.uType=uType; 14 | this.start=start; 15 | this.len=len; 16 | this.text=text; 17 | this.doubleUp=doubleUp; 18 | this.doubleUpDone=doubleUpDone; 19 | //if (doubleUp) System.out.println(this.toString()); 20 | } 21 | public static UndoStep createSaveState() { 22 | return new UndoStep(MARK_SAVE, -1, -1, null, false, false); 23 | } 24 | 25 | 26 | public boolean isSaveState() { 27 | return uType==MARK_SAVE; 28 | } 29 | public boolean isAddOrRemove() { 30 | return uType<=2; 31 | } 32 | 33 | public void debug(String yeah) { 34 | Appendable ap=new StringBuilder(); 35 | debug(yeah, ap); 36 | System.out.println(ap); 37 | System.out.flush(); 38 | } 39 | public void debug(String yeah, Appendable sb) { 40 | try { 41 | sb.append(yeah); 42 | sb.append(this.toString()); 43 | } catch (Exception e) { 44 | throw new RuntimeException(e); 45 | } 46 | } 47 | public String toString() { 48 | return uType+" "+start+" "+len+" "+text+" "+doubleUp+"; "; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/io/EncryptionDecryptionStream.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.io; 2 | import java.io.BufferedReader; 3 | import java.io.Writer; 4 | import java.security.AlgorithmParameters; 5 | import java.util.Base64; 6 | import javax.crypto.Cipher; 7 | import javax.crypto.SecretKey; 8 | import javax.crypto.SecretKeyFactory; 9 | 10 | 11 | public class EncryptionDecryptionStream { 12 | 13 | private final Cipher cipher; 14 | 15 | public EncryptionDecryptionStream(char[] pass, CharSequence keySize, CharSequence paramsBase64) throws Exception { 16 | this(pass, Integer.parseInt(keySize.toString()), paramsBase64); 17 | } 18 | 19 | public EncryptionDecryptionStream(char[] pass, int keySize, CharSequence paramsBase64) throws Exception { 20 | String algorithm=EncryptionStream.pbeAlgorithm+keySize; 21 | AlgorithmParameters algParams=AlgorithmParameters.getInstance(algorithm); 22 | { 23 | byte[] params=EncryptionStream.base64Decoder.decode( 24 | paramsBase64.toString().getBytes(EncryptionStream.utf8) 25 | ); 26 | algParams.init(params); 27 | } 28 | cipher=Cipher.getInstance(algorithm); 29 | cipher.init( 30 | Cipher.DECRYPT_MODE, 31 | EncryptionStream.getSecretKey(pass, keySize, algorithm), 32 | algParams 33 | ); 34 | } 35 | 36 | public String decrypt(CharSequence base64Data) throws Exception { 37 | return new String( 38 | cipher.doFinal( 39 | EncryptionStream.base64Decoder.decode( 40 | base64Data.toString().getBytes(EncryptionStream.utf8) 41 | ) 42 | ), 43 | EncryptionStream.utf8 44 | ); 45 | } 46 | 47 | public static void decrypt(BufferedReader reader, char[] password, Appendable output) throws Exception { 48 | // Get inputs: 49 | final String 50 | marker=reader.readLine(), 51 | keySize=reader.readLine().trim(), 52 | params=reader.readLine(); 53 | if (!EncryptionStream.matches(marker)) 54 | throw new RuntimeException("Not marker: "+marker); 55 | 56 | // And decrypt: 57 | EncryptionDecryptionStream inStream= 58 | new EncryptionDecryptionStream(password, keySize, params); 59 | String line; 60 | while ((line=reader.readLine())!=null) 61 | output.append(inStream.decrypt(line)); 62 | } 63 | 64 | public static void main(String[] args) throws Exception { 65 | test(); 66 | } 67 | 68 | ////////////// 69 | // TESTING: // 70 | ////////////// 71 | 72 | private static void test() throws Exception { 73 | 74 | // BUILD DATA: 75 | Writer writer=new java.io.StringWriter(); 76 | char[] password="HelloWorld".toCharArray(); 77 | { 78 | int keySize=128; 79 | try ( 80 | EncryptionStream es=new EncryptionStream(writer, keySize, password); 81 | ) { 82 | es.append("hi"); 83 | for (int i=0; i<1000; i++) 84 | es.append((i%10==0 ?"\n" :"")+"yo"+i); 85 | es.append("DONE"); 86 | } 87 | System.out.print(writer.toString()); 88 | } 89 | decrypt( 90 | new java.io.BufferedReader( 91 | new java.io.StringReader(writer.toString()) 92 | ), 93 | password, System.out 94 | ); 95 | System.out.flush(); 96 | 97 | } 98 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/io/EncryptionParams.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.io; 2 | 3 | public class EncryptionParams { 4 | public char[] pass={}; 5 | public int bits; 6 | public @Override String toString() { 7 | return pass.length + " " + bits; 8 | } 9 | public void nullify() { 10 | for (int i=0; i 0) A plaintext flag indicating file encryption, plus newline 15 | *

1) The key size 16 | *

2) The cypher parameters, which includes a salt, plus newline 17 | *

3) A series of lines: Each line is a buffered chunk of the file, no larger than 18 | * our specified buffer length. The encryption bytes are base64-encoded. 19 | * Thus each line should be decryptable by itself. 20 | *

21 | * Note that we only generate our salted key once, instead of once per buffer chunk. As 22 | * far as we know this is secure enough. 23 | */ 24 | public class EncryptionStream implements LightweightWriter { 25 | 26 | ///////////////////// 27 | // True constants: // 28 | ///////////////////// 29 | 30 | 31 | final static String pbeAlgorithm="PBEWithHmacSHA256AndAES_"; 32 | final static Charset utf8=Charset.forName("UTF-8"); 33 | final static int paramsLength=104; 34 | final static Base64.Decoder base64Decoder=Base64.getDecoder(); 35 | 36 | public final static String encryptionFlag= 37 | "<<[*******]>> // ENCRYPTED BY KLONK // <<[*******]>>"; 38 | private final static int iterations=1024*1024; 39 | private final static int bufferSize=1024; 40 | private final static Base64.Encoder base64Encoder=Base64.getEncoder(); 41 | 42 | ///////////////////////// 43 | // Instance variables: // 44 | ///////////////////////// 45 | 46 | private final Writer writer; 47 | private final Cipher cipher; 48 | private final StringBuilder buffer=new StringBuilder(); 49 | 50 | public static SecretKey getSecretKey(char[] pass, int keySize, String algorithm) throws Exception { 51 | byte[] salt=new byte[8]; 52 | new SecureRandom().nextBytes(salt); 53 | return SecretKeyFactory.getInstance(algorithm) 54 | .generateSecret( 55 | new PBEKeySpec(pass, salt, iterations, keySize) 56 | ); 57 | } 58 | 59 | public static boolean matches(String firstLine) { 60 | return firstLine.trim().equals(encryptionFlag); 61 | } 62 | 63 | public EncryptionStream(Writer writer, int keySize, char[] pass) throws Exception { 64 | this.writer=writer; 65 | String algorithm=pbeAlgorithm+keySize; 66 | this.cipher = Cipher.getInstance(algorithm); 67 | cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(pass, keySize, algorithm)); 68 | byte[] params = cipher.getParameters().getEncoded(); 69 | // Note: After JDK 10 this length changed, because the algorithm changed, and it's only 70 | // half-backwards compatible: 71 | // - JDK 13 can read JDK 10-encrypted files 72 | // - JDK 10 cannot read JDK 13-encrypted files :( 73 | //if (params.length != paramsLength) 74 | // throw new Exception("Cannot encrypt because params are wrong "+params.length); 75 | writer 76 | .append(encryptionFlag).append("\n") 77 | .append(String.valueOf(keySize)).append("\n") 78 | .append(base64Encoder.encodeToString(params)).append("\n") 79 | .flush(); 80 | } 81 | 82 | public void append(CharSequence data) throws Exception { 83 | buffer.append(data); 84 | flush(false); 85 | } 86 | public void close() { 87 | try { 88 | flush(true); 89 | } catch (Exception e) { 90 | throw new RuntimeException(e); 91 | } 92 | } 93 | public void flush() throws java.io.IOException { 94 | flush(false); 95 | } 96 | 97 | private void flush(boolean force) { 98 | try { 99 | if (force || buffer.length()>bufferSize){ 100 | writer.append( 101 | base64Encoder.encodeToString( 102 | cipher.doFinal( 103 | buffer.toString().getBytes(utf8) 104 | ) 105 | ) 106 | ).append("\n"); 107 | buffer.setLength(0); 108 | } 109 | } catch (Exception e) { 110 | throw new RuntimeException(e); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/io/FileMetaData.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.io; 2 | 3 | public class FileMetaData { 4 | public static String UTF8="UTF-8", 5 | UTF16BE="UTF-16BE", 6 | UTF16LE="UTF-16LE"; 7 | public String delimiter; 8 | public boolean hasTabs=false; 9 | public String encoding=UTF8; 10 | public boolean encodingNeedsBOM=false; 11 | public int readOffset=0; 12 | public EncryptionParams encryption=null; 13 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/io/KLog.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.io; 2 | import java.io.*; 3 | import org.tmotte.klonk.config.KHome; 4 | 5 | public class KLog { 6 | 7 | private final long start=System.nanoTime(); 8 | private KHome home; 9 | private PrintWriter commandLineWriter; 10 | private java.text.SimpleDateFormat sdformat; 11 | 12 | ////////////////// 13 | // CONSTRUCTOR: // 14 | ////////////////// 15 | 16 | public KLog(OutputStream os){ 17 | this.commandLineWriter=new PrintWriter(new OutputStreamWriter(os)); 18 | sdformat=new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS "); 19 | } 20 | public KLog(KHome home, String pid){ 21 | this.home=home; 22 | sdformat=new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS '"+pid+"\t'"); 23 | } 24 | 25 | /////////////////////// 26 | // PUBLIC FUNCTIONS: // 27 | /////////////////////// 28 | 29 | public void log(String s) { 30 | logPlain( 31 | sdformat.format(new java.util.Date())+s 32 | ); 33 | } 34 | public void error(String s) { 35 | log("ERROR: "+s); 36 | } 37 | public void log(Throwable e) { 38 | logError(e); 39 | } 40 | public void log(Throwable e, String s) { 41 | error(s); 42 | logError(e); 43 | } 44 | public File getLogFile(){ 45 | return home.nameFile("log.txt"); 46 | } 47 | 48 | ////////////////////// 49 | // PRIVATE METHODS: // 50 | ////////////////////// 51 | 52 | private void logError(Throwable e) { 53 | PrintWriter pw=getWriter(); 54 | if (pw!=null) 55 | try { 56 | e.printStackTrace(pw); 57 | pw.flush(); 58 | } finally { 59 | close(pw); 60 | } 61 | } 62 | private void logPlain(String s) { 63 | PrintWriter pw=getWriter(); 64 | if (pw!=null) 65 | try { 66 | pw.println(s); 67 | pw.flush(); 68 | } finally { 69 | close(pw); 70 | } 71 | } 72 | private void close(PrintWriter p){ 73 | if (commandLineWriter!=null) 74 | return; 75 | p.close(); 76 | } 77 | private PrintWriter getWriter() { 78 | if (commandLineWriter!=null) 79 | return commandLineWriter; 80 | File logFile=getLogFile(); 81 | try { 82 | return new PrintWriter(new OutputStreamWriter(new FileOutputStream(logFile, true))); 83 | } catch (Exception e) { 84 | System.err.println("Could not create log file "+logFile.getAbsolutePath()); 85 | e.printStackTrace(); 86 | return null; 87 | } 88 | } 89 | 90 | /////////// 91 | // TEST: // 92 | /////////// 93 | 94 | public static void main(String[] args) { 95 | KLog log=new KLog(System.out); 96 | log.log("BARF"); 97 | System.out.flush(); 98 | 99 | } 100 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/io/LightweightWriter.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.io; 2 | 3 | public interface LightweightWriter extends java.io.Closeable, java.io.Flushable { 4 | public void append(CharSequence cs) throws Exception; 5 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/io/LockInterface.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.io; 2 | import org.tmotte.klonk.config.msg.Setter; 3 | import java.util.List; 4 | 5 | public interface LockInterface { 6 | public boolean lockOrSignal(String[] fileNames); 7 | public void startListener(Setter> fileReceiver); 8 | public Runnable getLockRemover(); 9 | } 10 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/io/LockTest.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.io; 2 | import java.util.Random; 3 | import java.security.SecureRandom; 4 | 5 | public class LockTest { 6 | public static void main(String[] args) throws Exception { 7 | Random random=new SecureRandom(); 8 | String fileName=args[0]; 9 | String index=args[1]; 10 | Locker locker=new Locker(new java.io.File(fileName), new KLog(System.out)); 11 | int sleep1=random.nextInt(2000), sleep2=random.nextInt(2000); 12 | Thread.sleep(sleep1); 13 | if (locker.lock()) { 14 | System.out.println("\n"+index+": Got lock after "+sleep1); 15 | Thread.sleep(sleep2); 16 | locker.unlock(); 17 | System.out.println(index+": Released lock after "+sleep2); 18 | } 19 | else 20 | System.out.println("\n"+index+": Did not get lock after "+sleep1); 21 | System.out.flush(); 22 | } 23 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/io/Locker.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.io; 2 | import java.io.BufferedReader; 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.FileOutputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStream; 8 | import java.io.PrintWriter; 9 | import java.io.RandomAccessFile; 10 | import java.nio.channels.FileChannel; 11 | import java.nio.channels.FileLock; 12 | import java.nio.file.FileSystem; 13 | import java.nio.file.FileSystems; 14 | import java.util.LinkedList; 15 | import java.util.List; 16 | 17 | class Locker { 18 | private File lockFile; 19 | private FileLock lock; 20 | private FileOutputStream fos; 21 | private KLog log; 22 | 23 | public Locker(File lockFile, KLog log) { 24 | this.lockFile=lockFile; 25 | this.log=log; 26 | } 27 | 28 | public boolean lock() { 29 | try { 30 | fos=new FileOutputStream(lockFile); 31 | FileChannel fc=fos.getChannel(); 32 | lock=fc.tryLock(); 33 | if (lock==null){ 34 | log.log("Locker: Failed, got a null value on lockFile: "+lockFile); 35 | return false; 36 | } 37 | fos.write(1); 38 | return true; 39 | } catch (Exception e) { 40 | log.log(e); 41 | return false; 42 | } 43 | } 44 | public void unlock() { 45 | try { 46 | lock.release(); 47 | fos.close(); 48 | lockFile.delete(); 49 | } catch (Exception e) { 50 | log.log(e); 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/io/Printing.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.io; 2 | import java.awt.print.PrinterException; 3 | import java.awt.print.PrinterJob; 4 | import java.awt.print.Printable; 5 | import java.awt.print.PageFormat; 6 | import java.awt.print.Paper; 7 | import java.awt.Color; 8 | import javax.swing.JTextArea; 9 | 10 | public class Printing { 11 | 12 | private static Paper lastPaper; 13 | 14 | public static boolean print(JTextArea jta) { 15 | PrinterJob job=PrinterJob.getPrinterJob(); 16 | 17 | //Switch colors so that we get pure black & white, 18 | //not greyscale or whatever. We'll switch them back 19 | //in a moment: 20 | Color fore=jta.getForeground(), 21 | back=jta.getBackground(); 22 | jta.setForeground(Color.BLACK); 23 | jta.setBackground(Color.WHITE); 24 | 25 | try { 26 | job.setPrintable(jta.getPrintable(null, null)); 27 | PageFormat pf=job.defaultPage(); 28 | 29 | //Mainly we want some decent margins, and preserve 30 | //them between prints. Not going to bother persisting 31 | //them to disk though: 32 | if (lastPaper==null) { 33 | lastPaper=pf.getPaper(); 34 | int margin=72/4; 35 | int margin2=2*margin; 36 | lastPaper.setImageableArea( 37 | margin, margin, 38 | lastPaper.getWidth()-margin2, 39 | lastPaper.getHeight()-margin2 40 | ); 41 | } 42 | pf.setPaper(lastPaper); 43 | 44 | //Now show dialog and print. Note where we 45 | //save the selected paper format: 46 | PageFormat pf2=job.pageDialog(pf); 47 | if (pf2!=pf) { 48 | lastPaper=pf2.getPaper(); 49 | job.print(); 50 | return true; 51 | } 52 | return false; 53 | 54 | //This also seems like a nice option, but it doesn't 55 | //allow for setting page format. 56 | //if (job.printDialog()) 57 | // job.print(); 58 | 59 | } catch (Exception e) { 60 | throw new RuntimeException(e); 61 | } finally { 62 | jta.setForeground(fore); 63 | jta.setBackground(back); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/ConnectionParse.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | import java.util.HashMap; 3 | import java.util.Map; 4 | import org.tmotte.common.text.StringChunker; 5 | import java.util.regex.Pattern; 6 | import java.util.Set; 7 | 8 | /** 9 | * This is really just an extension of SSHConnections, should maybe be combined 10 | */ 11 | class ConnectionParse { 12 | 13 | private final Pattern sshPattern=Pattern.compile("ssh:/*", Pattern.CASE_INSENSITIVE); 14 | private final StringChunker chunker=new StringChunker(); 15 | 16 | protected SSHFile parse(SSHConnections connMgr, String uri) { 17 | String user=null, host=null, dirFile=null; 18 | chunker.reset(uri); 19 | 20 | //Walk past the ssh:(////), we don't need it: 21 | if (!chunker.find(sshPattern)) 22 | throw new RuntimeException(uri+" does not contain "+sshPattern); 23 | 24 | //Optionally get user@: 25 | if (chunker.find("@")) 26 | user=chunker.getUpTo(); 27 | 28 | //Get host: 29 | if (chunker.find(":")) 30 | host=chunker.getUpTo(); 31 | else 32 | if (chunker.find("/")){ 33 | host=chunker.getUpTo(); 34 | chunker.reset("/"+chunker.getRest()); 35 | } 36 | else 37 | host=chunker.getRest(); 38 | 39 | //Go back and try again on user if we need to: 40 | if (user==null) 41 | user=connMgr.inferUserForHost(host); 42 | if (user==null) 43 | return null; 44 | 45 | //Now make the SSH object & get file name: 46 | SSH ssh=connMgr.getOrCreate(user, host); 47 | if (ssh==null || !ssh.verifyConnection()) 48 | return null; 49 | 50 | return startParse(ssh, chunker.getRest(), chunker); 51 | } 52 | 53 | private SSHFile startParse(SSH ssh, String toParse, StringChunker reuse) { 54 | if (toParse.contains("~")) 55 | toParse=toParse.replace("~", ssh.getTildeFix()); 56 | SSHFile result=parse(ssh, null, reuse.reset(toParse)); 57 | if (result==null) 58 | //No name given, default to "~" 59 | result=new SSHFile(ssh, null, ssh.getTildeFix()); 60 | return result; 61 | } 62 | private SSHFile parse(SSH ssh, SSHFile parent, StringChunker left) { 63 | if (!left.find("/")){ 64 | String remains=left.getRest(); 65 | if (remains==null) 66 | return parent; 67 | else { 68 | remains=remains.trim(); 69 | if (remains.equals("")) 70 | return parent; 71 | return new SSHFile(ssh, parent, remains); 72 | } 73 | } 74 | else { 75 | String name=left.getUpTo(); 76 | if (name.equals("") && parent==null) 77 | name="/"; 78 | return parse( 79 | ssh, 80 | new SSHFile(ssh, parent, name), 81 | left 82 | ); 83 | } 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/IFileGet.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | public interface IFileGet { 3 | public java.io.File get(String name); 4 | } 5 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/IUserPass.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | public interface IUserPass { 3 | public boolean get(String user, String host, boolean authFail, boolean needsPassword); 4 | public String getUser(); 5 | public String getPass(); 6 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/MeatCounter.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | 3 | /** 4 | * This implements the take-a-number system at the grocery store, thus preventing 5 | * "thread starvation". It's very clever, but: 6 | *
7 | * Everyone must play nice and answer when their number is called. 8 | * If a thread fails to call unlock(), the whole system seizes up thereafter, so lock() 9 | * and unlock() should always be used with try-finally. When sleeping between lock 10 | * attempts, if the thread is sent an interrupt() we ignore that and keep going. 11 | */ 12 | public class MeatCounter { 13 | 14 | //Yes, these ints will eventually wrap around to Integer.MIN_VALUE, which is ok: 15 | private volatile int nextUp=1; 16 | private int lastTicket=0; 17 | private final long spintime; 18 | 19 | /** 20 | * @param spintime This is how long we tell a Thread to sleep when it's 21 | * waiting for a lock. If the value is <= 0, we don't sleep at all. 22 | */ 23 | public MeatCounter(long spintime){ 24 | this.spintime=spintime; 25 | } 26 | 27 | /** Unsynchronized because it should only be called when you hold the lock. */ 28 | public void unlock() { 29 | nextUp++; 30 | //mylog(nextUp, "UNLOCK "); 31 | } 32 | 33 | /** 34 | * @return true if we received an Exception when sleeping between 35 | * lock attempts, most likely an InterruptedException. We will still 36 | * keep sleeping until we get the lock, however. 37 | */ 38 | public boolean lock(String name) { 39 | final int myTurn=takeANumber(); 40 | //mylog(myTurn, "PICKED: "+name); 41 | boolean interrupted=false; 42 | while (nextUp!=myTurn) { 43 | //mylog(myTurn, "WAIT: "+name); 44 | if (spintime>0) 45 | try {Thread.sleep(spintime);} 46 | catch (Exception e) { 47 | //We have no choice but to keep trying, since otherwise 48 | //we end up with a lock that will never be unlocked. 49 | interrupted=true; 50 | } 51 | } 52 | //mylog(myTurn, "LOCKED: "+name); 53 | return interrupted; 54 | } 55 | 56 | /** 57 | * This must be synchronized because the ++ operator is not atomic, 58 | * nor is it "more atomic" just because you toss in a volatile keyword. 59 | */ 60 | private synchronized int takeANumber() { 61 | return ++lastTicket; 62 | } 63 | private static void mylog(int number, String msg) { 64 | System.out.println("MeatCounter: "+number+" "+msg); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/SFTP.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | import com.jcraft.jsch.Session; 3 | import com.jcraft.jsch.Channel; 4 | import com.jcraft.jsch.ChannelSftp; 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.io.OutputStream; 9 | import java.io.OutputStreamWriter; 10 | import com.jcraft.jsch.SftpATTRS; 11 | import java.util.List; 12 | import java.util.ArrayList; 13 | 14 | 15 | /** 16 | * SFTP in jsch is not thread-safe. Worse yet, you can try to use synchronized blocks to ameliorate, but it will go right around them. 17 | * So I have a locking system in place to work around this, and it's pretty good. The only catch is that getInputStream() & getOutputStream() 18 | * get called from far away and it's hard to lock them down. 19 | */ 20 | class SFTP { 21 | 22 | private SSH ssh; 23 | private ChannelSftp channel; 24 | private MeatCounter mc; 25 | 26 | SFTP(SSH ssh, MeatCounter mc) { 27 | this.ssh=ssh; 28 | this.mc=mc; 29 | } 30 | 31 | //////////////////////////////// 32 | // PACKAGE-PRIVATE FUNCTIONS: // 33 | //////////////////////////////// 34 | 35 | 36 | InputStream getInputStream(String file) throws Exception { 37 | try { 38 | return getChannel(file).get(file); 39 | } finally { 40 | unlock(); 41 | } 42 | } 43 | OutputStream getOutputStream(String file) throws Exception { 44 | try { 45 | return getChannel(file).put(file); 46 | } finally { 47 | unlock(); 48 | } 49 | } 50 | SSHFileAttr getAttributes(String file) { 51 | ssh.userNotify.log("SFTP.getAttributes "+file); 52 | try { 53 | ChannelSftp ch=getChannel(file); 54 | if (ch==null) 55 | return null; 56 | SSHFileAttr sfa=new SSHFileAttr(ch.stat(file)); 57 | ssh.userNotify.log(sfa.toString()); 58 | return sfa; 59 | } catch (Exception e) { 60 | try {close();} catch (Exception e2) {ssh.userNotify.log(e2);} 61 | String msg=e.getMessage(); 62 | if (msg!=null && msg.equals("No such file")){ 63 | ssh.userNotify.log(msg+file); 64 | return null; 65 | //This never worked out, just made a mess with ->new folder ->rename in file dialog. Unfortunately 66 | //it also means that we call this function again & again because we have null and want a value. 67 | //return new SSHFileAttr(); 68 | } 69 | else 70 | if (canIgnore(e, file, "getAttributes")) 71 | return null; 72 | else 73 | throw new RuntimeException("Failed to get stat on: "+file, e); 74 | } finally { 75 | //ssh.userNotify.log("SFTP.getAttributes "+Thread.currentThread().hashCode()+" "+file+" COMPLETE "); 76 | unlock(); 77 | } 78 | } 79 | 80 | private static String[] noFiles={}; 81 | String[] listFiles(String file) { 82 | ssh.userNotify.log("SFTP.listFiles "+Thread.currentThread().hashCode()+" "+file); 83 | ChannelSftp ch=getChannel(file); 84 | try { 85 | List vv=ch.ls(file); 86 | String[] values=new String[vv.size()]; 87 | int count=0; 88 | if (vv!=null) 89 | for (int ii=0; ii-1; i--) 101 | if (values[i]!=null){ 102 | realVals[count-1]=values[i]; 103 | count--; 104 | } 105 | return realVals; 106 | } catch (Exception e) { 107 | try {close();} catch (Exception e2) {ssh.userNotify.log(e2);} 108 | if (!canIgnore(e, file, "listFiles")) 109 | ssh.userNotify.alert(e, "Failed to list files for: "+file); 110 | return noFiles; 111 | } finally { 112 | //ssh.userNotify.log("SFTP.listFiles "+Thread.currentThread().hashCode()+" "+file+" COMPLETE "); 113 | unlock(); 114 | } 115 | } 116 | 117 | void close() throws Exception { 118 | if (channel!=null) { 119 | if (channel.isConnected()) 120 | try {channel.disconnect();} catch (Exception e) {} 121 | channel=null; 122 | } 123 | } 124 | 125 | //////////////////////// 126 | // PRIVATE FUNCTIONS: // 127 | //////////////////////// 128 | 129 | /** 130 | * As we overload the connection, random stuff blows up but we can ignore it because 131 | * this is just the file chooser whacking us over the head and it will be just fine. 132 | */ 133 | private boolean canIgnore(Throwable e, String file, String function) { 134 | e=getCause(e); 135 | String msg=e.getMessage(); 136 | if ( 137 | (e instanceof java.lang.InterruptedException) || 138 | (e instanceof java.io.InterruptedIOException) || 139 | (e instanceof java.lang.IndexOutOfBoundsException) || 140 | (msg!=null && ( 141 | msg.contains("Permission denied") || 142 | msg.contains("No such file") || 143 | msg.contains("inputstream is closed") 144 | )) 145 | ){ 146 | ssh.userNotify.log("SFTP Hidden fail: "+function+" "+e+" ..."+file); 147 | ssh.userNotify.log(e); 148 | return true; 149 | } 150 | else if ((e instanceof java.io.IOException) && e.getMessage().equals("Pipe closed")){ 151 | ssh.userNotify.log("SFTP Apparently closed: "+e+" ..."+file); 152 | try {ssh.close();} catch (Exception e2) {ssh.userNotify.log(e2);} 153 | return false; 154 | } 155 | else 156 | return false; 157 | } 158 | private Throwable getCause(Throwable e) { 159 | Throwable e1=e.getCause(); 160 | return e1==null ?e :getCause(e1); 161 | } 162 | 163 | 164 | private ChannelSftp getChannel(String file) { 165 | lock(file); 166 | try { 167 | if (channel==null || !channel.isConnected()) 168 | channel=makeChannel(); 169 | } catch (Exception e) { 170 | unlock(); 171 | throw new RuntimeException(e); 172 | } 173 | return channel; 174 | } 175 | 176 | private ChannelSftp makeChannel() throws Exception { 177 | Session session=ssh.getSession(); 178 | if (session==null) 179 | return null; 180 | session.setConfig("compression.s2c", "zlib@openssh.com,zlib,none");//Fails if we don't do this - not that I understand it 181 | session.setConfig("compression.c2s", "zlib@openssh.com,zlib,none"); 182 | ChannelSftp c=(ChannelSftp)session.openChannel("sftp"); 183 | //setBulkRequests() seems to help... not sure. Might be making things worse: 184 | c.setBulkRequests(100); 185 | c.connect(); 186 | return c; 187 | } 188 | 189 | private void lock(String file) { 190 | mc.lock(file); 191 | } 192 | private void unlock() { 193 | mc.unlock(); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/SSHCommandLine.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | import java.io.BufferedReader; 3 | import java.io.InputStreamReader; 4 | import java.io.File; 5 | import java.io.FilenameFilter; 6 | import java.io.FileFilter; 7 | import java.nio.file.Path; 8 | import java.util.List; 9 | import java.util.LinkedList; 10 | import org.tmotte.klonk.config.option.SSHOptions; 11 | import org.tmotte.klonk.config.msg.UserNotify; 12 | 13 | public class SSHCommandLine { 14 | 15 | public interface ArgHandler { 16 | /** Return a new index, or -1 to indicate nothing found */ 17 | public int handle(String[] args, int currIndex); 18 | public String document(); 19 | } 20 | public SSH ssh; 21 | public SSHConnections connections; 22 | public SSHFile sshFile; 23 | 24 | private ArgHandler argHandler; 25 | 26 | public SSHCommandLine(String[] args) throws Exception { 27 | this(args, null); 28 | } 29 | public SSHCommandLine(String[] args, ArgHandler argHandler) throws Exception { 30 | this.argHandler=argHandler; 31 | int max=0; 32 | String user=null, host=null, knownHosts=null, pass=null, privateKeys=null, fileName=null; 33 | for (int i=0; i [-k knownhostsfile] [-p pass] [-r privatekeyfile] "+ 87 | (argHandler==null ?"" :argHandler.document()) 88 | ); 89 | System.exit(1); 90 | } 91 | 92 | //////////////////////// 93 | // INTERACTIVE LOGIN: // 94 | //////////////////////// 95 | 96 | private static class Login implements IUserPass { 97 | String user, pass; 98 | public Login(String user, String pass) { 99 | this.user=user; 100 | this.pass=pass; 101 | } 102 | final BufferedReader br=new BufferedReader(new InputStreamReader(System.in)); 103 | public boolean get(String u, String host, boolean authFail, boolean needsPassword) { 104 | user=u; 105 | if (user!=null && pass !=null) 106 | return true; 107 | try { 108 | System.out.println("Host: "+host); 109 | 110 | System.out.print("User: "); 111 | if (user!=null && !user.trim().equals("")) 112 | System.out.print("<"+user+"> press enter to keep or: "); 113 | u=br.readLine().trim(); 114 | if (!u.trim().equals("")) 115 | this.user=u; 116 | 117 | System.out.print("Pass: "); 118 | String p=br.readLine().trim(); 119 | if (p!=null && !p.trim().equals("")) 120 | this.pass=p; 121 | 122 | return user!=null && !user.trim().equals("") && 123 | pass!=null && !pass.trim().equals(""); 124 | } catch (Exception e) { 125 | throw new RuntimeException(e); 126 | } 127 | } 128 | public String getUser(){return user;} 129 | public String getPass(){return pass;} 130 | } 131 | 132 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/SSHConnections.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | import java.io.File; 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.LinkedList; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.regex.Pattern; 10 | import org.tmotte.klonk.config.option.SSHOptions; 11 | import org.tmotte.common.text.StringChunker; 12 | import org.tmotte.klonk.config.msg.UserNotify; 13 | 14 | /** This does not establish a connection. It only collects potential connections. Not thread-safe. */ 15 | public class SSHConnections { 16 | 17 | private final Map> conns=new HashMap<>(); 18 | private final ConnectionParse parser=new ConnectionParse(); 19 | private Set users=new HashSet<>(); 20 | 21 | private String knownHosts, privateKeys, openSSHConfig; 22 | private String defaultFilePerms, defaultDirPerms; 23 | private IUserPass iUserPass; 24 | private UserNotify userNotify; 25 | 26 | private IFileGet iFileGet=new IFileGet(){ 27 | public File get(String uri) { 28 | if (is(uri)) return getSSHFile(uri); 29 | else return new File(uri); 30 | } 31 | }; 32 | 33 | ///////////// 34 | // CREATE: // 35 | ///////////// 36 | 37 | public SSHConnections (UserNotify userNotify) { 38 | this.userNotify=userNotify; 39 | } 40 | public SSHConnections withOptions(SSHOptions opts){ 41 | this.knownHosts=opts.getKnownHostsFilename(); 42 | this.privateKeys=opts.getPrivateKeysFilename(); 43 | this.defaultFilePerms=opts.getDefaultFilePermissions(); 44 | this.defaultDirPerms=opts.getDefaultDirPermissions(); 45 | this.openSSHConfig=opts.getOpenSSHConfigFilename(); 46 | for (Map map: conns.values()) 47 | for (SSH ssh: map.values()) 48 | configure(ssh); 49 | return this; 50 | } 51 | public SSHConnections withLogin(IUserPass iUserPass) { 52 | this.iUserPass=iUserPass; 53 | return this; 54 | } 55 | 56 | ///////////// 57 | // PUBLIC: // 58 | ///////////// 59 | 60 | public void close(String host) { 61 | Map perHost=conns.get(host); 62 | for (SSH ssh: perHost.values()) 63 | ssh.close(); 64 | } 65 | public void close() throws Exception { 66 | for (String host: conns.keySet()){ 67 | Map forHost=conns.get(host); 68 | for (String user: forHost.keySet()) 69 | forHost.get(user).close(); 70 | } 71 | try { 72 | } catch (Exception e) { 73 | e.printStackTrace(); 74 | } 75 | } 76 | public boolean is(String uri) { 77 | return uri!=null && ( 78 | uri.startsWith("ssh:") || 79 | uri.startsWith("SSH:") 80 | ); 81 | } 82 | public SSHFile getSSHFile(String uri) { 83 | return parser.parse(this, uri); 84 | } 85 | public SSH getOrCreate(String user, String host) { 86 | Map perHost=getForHost(host); 87 | SSH ssh=perHost.get(user); 88 | if (ssh==null) { 89 | ssh= 90 | configure(new SSH(user, host, userNotify)) 91 | .withIUserPass(iUserPass); 92 | if (!ssh.verifyConnection()) 93 | return null; 94 | perHost.put(user, ssh); 95 | users.add(user); 96 | } 97 | return ssh; 98 | } 99 | public String toString() { 100 | return conns.toString(); 101 | } 102 | public IFileGet getFileResolver() { 103 | return iFileGet; 104 | } 105 | public List getConnectedHosts() { 106 | List h=new LinkedList<>(); 107 | for (String s: conns.keySet()){ 108 | Map zzz=conns.get(s); 109 | boolean is=false; 110 | for (String u: zzz.keySet()) 111 | if (!is && zzz.get(u).isConnected()) 112 | is=true; 113 | if (is) h.add(s); 114 | } 115 | return h; 116 | } 117 | 118 | ////////////////////// 119 | // PACKAGE PRIVATE: // 120 | ////////////////////// 121 | 122 | /** This is here only for the connection parser: */ 123 | String inferUserForHost(String host) { 124 | 125 | //If there is only one user so far, assume it's them: 126 | if (users.size()==1) 127 | return users.iterator().next(); 128 | 129 | //If there is an existing connection for that host, 130 | //use the user for it; else tell them to just log in. 131 | //Note that when they login, they may use a new password. 132 | Map perHost=getForHost(host); 133 | if (perHost.size()==1){ 134 | SSH ssh=perHost.values().iterator().next(); 135 | String s=ssh.getUser(); 136 | if (s!=null) 137 | return s; 138 | } 139 | 140 | if (iUserPass!=null && iUserPass.get(null, host, false, privateKeys==null)){ 141 | String user=iUserPass.getUser(); 142 | SSH ssh=getOrCreate(user, host); 143 | String pass=iUserPass.getPass(); 144 | if (pass!=null && !pass.trim().equals("")) 145 | ssh.withPassword(pass); 146 | return user; 147 | } 148 | else 149 | return null; 150 | } 151 | 152 | //////////////////////// 153 | // PRIVATE FUNCTIONS: // 154 | //////////////////////// 155 | 156 | private SSH configure(SSH ssh) { 157 | ssh 158 | .withKnown(knownHosts) 159 | .withPrivateKeys(privateKeys) 160 | .withOpenSSHConfig(openSSHConfig) 161 | .withDefaultPerms(defaultFilePerms, defaultDirPerms); 162 | return ssh; 163 | } 164 | private Map getForHost(String host) { 165 | Map perHost=conns.get(host); 166 | if (perHost==null){ 167 | perHost=new HashMap(); 168 | conns.put(host, perHost); 169 | } 170 | return perHost; 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/SSHExec.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | import com.jcraft.jsch.*; 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.io.PrintStream; 7 | import org.tmotte.klonk.config.msg.UserNotify; 8 | 9 | public class SSHExec { 10 | 11 | private SSH ssh; 12 | private MeatCounter mc; 13 | protected UserNotify userNotify; 14 | 15 | SSHExec(SSH ssh, MeatCounter mc, UserNotify userNotify) { 16 | this.ssh=ssh; 17 | this.mc=mc; 18 | this.userNotify=userNotify; 19 | } 20 | 21 | SSHExecResult exec(String command, boolean alertFail) { 22 | StringBuilder out=new StringBuilder(); 23 | ByteArrayOutputStream err=new ByteArrayOutputStream(512); 24 | int result=exec(command, out, err); 25 | if (err.size()>0 || result!=0) 26 | try { 27 | out.append(err.toString("utf-8")); 28 | result=1; 29 | String bad="SSH Failure: "+out.toString(); 30 | if (alertFail) 31 | userNotify.alert(bad); 32 | mylog(bad); 33 | } catch (Exception e) { 34 | throw new RuntimeException(e); 35 | } 36 | return new SSHExecResult(result==0, out.toString()); 37 | } 38 | private int exec(String command, Appendable out, Appendable err) throws Exception { 39 | ByteArrayOutputStream baos=new ByteArrayOutputStream(512); 40 | int result=exec(command, out, baos); 41 | try { 42 | err.append(baos.toString("utf-8")); 43 | } catch (Exception e) { 44 | throw new RuntimeException(e); 45 | } 46 | return result; 47 | } 48 | 49 | /** 50 | * @param sshErr DO NOT PASS SYSTEM.ERR/OUT TO THIS, IT WILL GET CHEWED TO PIECES (i.e. it will be closed by 51 | * the jsch library). 52 | * @return The output (typically 0,1,2) of the unix command, or -1 if we could not get a connection. 53 | */ 54 | private int exec(String command, Appendable out, OutputStream sshErr) { 55 | mylog(Thread.currentThread().hashCode()+" "+command); 56 | ChannelExec channel=getChannel(command); 57 | if (channel==null) 58 | return -1; 59 | try { 60 | channel.setCommand(command); 61 | channel.setErrStream(sshErr); 62 | channel.setOutputStream(null); 63 | InputStream in=channel.getInputStream(); 64 | channel.connect(); 65 | byte[] tmp=new byte[1024]; 66 | try { 67 | //This loop will spin rather heavily. 68 | while (!channel.isClosed()){ 69 | while (in.available()>0){ 70 | int i=in.read(tmp, 0, 1024); 71 | if (i<0) break; 72 | out.append(new String(tmp, 0, i)); 73 | } 74 | //Doing this will cause output to be lost. I do not know why. 75 | //try {Thread.sleep(10);} catch (Exception e) {e.printStackTrace();} 76 | } 77 | int result=channel.getExitStatus(); 78 | sshErr.flush(); 79 | //mylog(ssh.hashCode()+" "+command+" complete "+result); 80 | return result; 81 | } catch (Exception e) { 82 | closeOnFail(); 83 | throw new RuntimeException(e); 84 | } finally { 85 | releaseChannel(channel); 86 | //Doing this will cause output to be lost which doesn't make sense. Let's blame the compiler: 87 | //in.close(); 88 | } 89 | } catch (Exception e) { 90 | throw new RuntimeException("Failed to execute: "+command, e); 91 | } 92 | } 93 | 94 | 95 | private ChannelExec channel; 96 | private ChannelExec getChannel(String cmd) { 97 | mc.lock(cmd); 98 | try { 99 | //if (channel==null) channel=makeChannel(); 100 | return makeChannel(); 101 | } catch (Exception e) { 102 | mc.unlock(); 103 | throw new RuntimeException(e); 104 | } 105 | } 106 | private void releaseChannel(ChannelExec channel) { 107 | try { 108 | channel.disconnect(); 109 | //this.channel=null; 110 | } catch (Exception e) { 111 | throw new RuntimeException(e); 112 | } finally { 113 | mc.unlock(); 114 | } 115 | } 116 | private void closeOnFail() { 117 | if (channel!=null) 118 | channel.disconnect(); 119 | channel=null; 120 | } 121 | private ChannelExec makeChannel() throws Exception{ 122 | Session session=ssh.getSession(); 123 | if (session==null) 124 | return null; 125 | return (ChannelExec)session.openChannel("exec"); 126 | } 127 | 128 | 129 | private void mylog(String s) { 130 | userNotify.log("SSHExec: "+s); 131 | } 132 | 133 | 134 | } 135 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/SSHExecResult.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | class SSHExecResult { 3 | final boolean success; 4 | final String output; 5 | SSHExecResult(boolean success, String output){ 6 | this.success=success; 7 | this.output=output; 8 | logFail(); 9 | } 10 | public String toString() { 11 | return success+" "+output; 12 | } 13 | private void logFail() { 14 | } 15 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/SSHFileAttr.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | import com.jcraft.jsch.SftpATTRS; 3 | import java.io.File; 4 | import java.io.FileFilter; 5 | import java.io.FilenameFilter; 6 | import java.nio.file.Path; 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | import java.util.regex.Pattern; 10 | import org.tmotte.common.text.StringChunker; 11 | 12 | class SSHFileAttr { 13 | 14 | public final boolean isDirectory; 15 | public final long size; 16 | public final boolean exists; 17 | private final long lastMod;//This is the time without milliseconds 18 | 19 | SSHFileAttr(SftpATTRS orig){ 20 | isDirectory=orig.isDir(); 21 | lastMod=orig.getMTime(); 22 | size=orig.getSize(); 23 | exists=true; 24 | } 25 | SSHFileAttr(){ 26 | isDirectory=false; 27 | this.exists=false; 28 | this.size=0; 29 | this.lastMod=0; 30 | } 31 | 32 | long getLastModified() { 33 | return lastMod*1000; 34 | } 35 | 36 | public String toString() { 37 | return exists+" "+isDirectory+" "+size+" "+lastMod; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/WrapMap.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh; 2 | import java.util.HashMap; 3 | 4 | /** 5 | * This implements a basic Map cache where items are expired by age. When overcrowding occurs, 6 | * the oldest go overboard first. This would use generic types for both key & value, but we have 7 | * an internal array and those don't allow generics, thanks java :/ 8 | */ 9 | public class WrapMap { 10 | private final int max; 11 | private final long ageLimit; 12 | 13 | private final HashMap data; 14 | private final String[] names; 15 | private int limIndex=0; 16 | 17 | private final class IndexVal { 18 | B value; 19 | int index; 20 | long time; 21 | } 22 | 23 | public WrapMap(int max, long ageLimit) { 24 | this.max=max; 25 | this.ageLimit=ageLimit; 26 | data=new HashMap<>(this.max); 27 | names=new String[this.max]; 28 | } 29 | 30 | 31 | public synchronized B get(String a) { 32 | IndexVal vi=data.get(a); 33 | if (vi==null) 34 | return null; 35 | else 36 | if (System.currentTimeMillis()-vi.time > ageLimit){ 37 | data.remove(a); 38 | names[vi.index]=null; 39 | return null; 40 | } 41 | else 42 | return vi.value; 43 | } 44 | public synchronized void remove(String a) { 45 | IndexVal vi=data.get(a); 46 | if (vi!=null) { 47 | data.remove(a); 48 | names[vi.index]=null; 49 | } 50 | } 51 | 52 | public synchronized void put(String name, B b) { 53 | String other=names[limIndex]; 54 | if (other!=null) 55 | data.remove(other); 56 | 57 | IndexVal vi=data.get(name); 58 | if (vi!=null) 59 | names[vi.index]=null; 60 | else { 61 | vi=new IndexVal(); 62 | data.put(name, vi); 63 | } 64 | 65 | vi.value=b; 66 | vi.time=System.currentTimeMillis(); 67 | vi.index=limIndex; 68 | 69 | names[limIndex]=name; 70 | limIndex++; if (limIndex==max) limIndex=0; 71 | } 72 | public synchronized int size(){ 73 | return data.size(); 74 | } 75 | 76 | public synchronized void reset() { 77 | for (int i=0; i=perLine && count%perLine==0) 105 | ps.println(); 106 | } 107 | ps.println(); 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/test/FileListing.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh.test; 2 | import org.tmotte.klonk.ssh.SSHCommandLine; 3 | import org.tmotte.klonk.ssh.SSHConnections; 4 | import java.io.File; 5 | 6 | public class FileListing { 7 | public static void main(String[] args) throws Exception { 8 | SSHCommandLine cmd=new SSHCommandLine(args); 9 | try { 10 | File file=cmd.sshFile; 11 | mylog(file); 12 | System.out.println("Children..."); 13 | for (File f: file.listFiles()) 14 | mylog(f); 15 | /* 16 | while ((file=file.getParentFile())!=null) { 17 | System.out.println("Parent:"); 18 | mylog(file); 19 | for (File f: file.listFiles()){ 20 | System.out.println("Child:"); 21 | mylog(f); 22 | } 23 | } 24 | */ 25 | } finally { 26 | cmd.connections.close(); 27 | } 28 | } 29 | private static void mylog(File f) { 30 | long last=f.lastModified(); 31 | System.out.println( 32 | String.format("test.FilesListing: Dir: %-6s %s %s", ""+f.isDirectory(), new java.util.Date(f.lastModified()), f.getAbsolutePath()) 33 | ); 34 | } 35 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/test/FileSave.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh.test; 2 | import org.tmotte.klonk.ssh.SSHCommandLine; 3 | import org.tmotte.klonk.ssh.SSHFile; 4 | import org.tmotte.klonk.ssh.SSHConnections; 5 | import java.io.File; 6 | import java.io.OutputStream; 7 | import java.io.OutputStreamWriter; 8 | import java.io.InputStream; 9 | import java.io.InputStreamReader; 10 | 11 | 12 | public class FileSave { 13 | public static void main(String[] args) throws Exception { 14 | SSHCommandLine cmd=new SSHCommandLine(args); 15 | SSHFile file=cmd.sshFile; 16 | try { 17 | char[] readBuffer=new char[1024 * 20]; 18 | for (int limit=1; limit<10; limit++){ 19 | try ( 20 | OutputStream output=file.getOutputStream(); 21 | OutputStreamWriter outw=new OutputStreamWriter(output, "utf-8"); 22 | ){ 23 | if (!file.exists()) 24 | throw new RuntimeException("What do you mean it doesn't exist"); 25 | for (int i=0; i0){ 41 | String s=new String(readBuffer, 0, charsRead); 42 | System.out.print(s); 43 | System.out.flush(); 44 | } 45 | } 46 | System.out.println("File read"); 47 | } 48 | file.delete(); 49 | } finally { 50 | cmd.connections.close(); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/ssh/test/TestCaching.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.ssh.test; 2 | import org.tmotte.klonk.ssh.WrapMap; 3 | 4 | public class TestCaching { 5 | public static void main(String[] args) throws Exception { 6 | java.util.Random random=new java.util.Random(System.currentTimeMillis()); 7 | 8 | int maxSize=100; 9 | int maxTime=200; 10 | int maxSleep=100; 11 | int tries=200; 12 | int maxval=199; 13 | String formatStr="%5s-%-6s "; 14 | int perLine=10; 15 | 16 | WrapMap wc=new WrapMap<>(maxSize, maxTime); 17 | { 18 | for (int count=0; countrightEdge, 41 | badY=w.ybottomEdge; 42 | if (badX) w.x=screen.x+120; 43 | if (badY) w.y=screen.y; 44 | 45 | boolean tooWide=w.x+w.width > rightEdge, 46 | tooTall=w.y+w.height > bottomEdge; 47 | if (tooWide) w.width =rightEdge - w.x; 48 | if (tooTall) w.height=bottomEdge - w.y; 49 | return w; 50 | } 51 | 52 | private static Rectangle getScreenBounds() { 53 | Rectangle bds = new Rectangle(); 54 | for (GraphicsDevice gd: GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) 55 | for (GraphicsConfiguration gc: gd.getConfigurations()) 56 | bds=bds.union(gc.getBounds()); 57 | return bds; 58 | } 59 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/About-Version.txt: -------------------------------------------------------------------------------- 1 | Klonk $version ©2013-2024 Troy Motte -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/About.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup; 2 | import java.awt.Component; 3 | import java.awt.Container; 4 | import java.awt.Dimension; 5 | import java.awt.Font; 6 | import java.awt.GraphicsEnvironment; 7 | import java.awt.Point; 8 | import java.awt.Rectangle; 9 | import java.awt.Window; 10 | import java.awt.event.ActionEvent; 11 | import java.awt.event.KeyAdapter; 12 | import java.awt.event.KeyEvent; 13 | import java.awt.event.KeyListener; 14 | import java.util.List; 15 | import java.util.Properties; 16 | import javax.swing.AbstractAction; 17 | import javax.swing.Action; 18 | import javax.swing.JButton; 19 | import javax.swing.JDialog; 20 | import javax.swing.JFrame; 21 | import javax.swing.JLabel; 22 | import javax.swing.JPanel; 23 | import javax.swing.JScrollBar; 24 | import javax.swing.JScrollPane; 25 | import javax.swing.JTextPane; 26 | import javax.swing.ScrollPaneConstants; 27 | import org.tmotte.common.io.Loader; 28 | import org.tmotte.common.swang.GridBug; 29 | import org.tmotte.common.swang.KeyMapper; 30 | import org.tmotte.common.text.StackTracer; 31 | import org.tmotte.klonk.config.PopupInfo; 32 | import org.tmotte.klonk.config.option.FontOptions; 33 | import org.tmotte.klonk.windows.Positioner; 34 | 35 | public class About { 36 | 37 | // DI: 38 | private PopupInfo pInfo; 39 | private FontOptions fontOptions; 40 | 41 | // Controls: 42 | private JDialog win; 43 | private JTextPane jtpLicense, jtpVersion, jtpJavaVersion; 44 | private JScrollPane jspLicense; 45 | private JButton btnOK; 46 | 47 | // State: 48 | private boolean initialized=false; 49 | 50 | ///////////////////// 51 | // PUBLIC METHODS: // 52 | ///////////////////// 53 | 54 | public About(PopupInfo pInfo, FontOptions fontOptions) { 55 | this.pInfo=pInfo; 56 | this.fontOptions=fontOptions; 57 | pInfo.addFontListener(fo -> setFont(fo)); 58 | } 59 | 60 | public void show() { 61 | init(); 62 | Positioner.set(pInfo.parentFrame, win); 63 | btnOK.requestFocusInWindow(); 64 | win.pack(); 65 | win.setVisible(true); 66 | win.toFront(); 67 | } 68 | private void setFont(FontOptions fo) { 69 | this.fontOptions=fo; 70 | if (win!=null){ 71 | fontOptions.getControlsFont().set(win); 72 | win.pack(); 73 | } 74 | } 75 | 76 | ////////////////////// 77 | // PRIVATE METHODS: // 78 | ////////////////////// 79 | 80 | private void init() { 81 | if (!initialized){ 82 | create(); 83 | layout(); 84 | listen(); 85 | initialized=true; 86 | } 87 | } 88 | 89 | private void create(){ 90 | win=new JDialog(pInfo.parentFrame, true); 91 | win.setTitle("About Klonk"); 92 | win.setPreferredSize(new Dimension(400,400)); 93 | 94 | jtpLicense=new JTextPane(); 95 | jtpVersion=new JTextPane(); 96 | jtpJavaVersion=new JTextPane(); 97 | btnOK=new JButton(); 98 | 99 | JTextPane[] jtps={jtpLicense, jtpVersion, jtpJavaVersion}; 100 | for (JTextPane jtp: jtps) { 101 | jtp.setEditable(false); 102 | jtp.setBorder(null); 103 | jtp.setOpaque(false); 104 | } 105 | Font font=jtpVersion.getFont().deriveFont(Font.BOLD); 106 | jtpVersion.setFont(font); 107 | jtpJavaVersion.setFont(font); 108 | 109 | jtpLicense.setContentType("text/html"); 110 | Properties props=new Properties(); 111 | try (java.io.InputStream is=getClass().getResourceAsStream("About-Version-Number.txt");) { 112 | props.load(is); 113 | } catch (Exception e) { 114 | throw new RuntimeException(e); 115 | } 116 | String number=props.getProperty("VERSION.KLONK"); 117 | String 118 | license=Loader.loadUTF8String(getClass(), "About-License.html"), 119 | version=Loader.loadUTF8String(getClass(), "About-Version.txt"), 120 | javaVersion="Running under Java version: "+System.getProperty("java.version"); 121 | license=license.replaceAll("", ""); 122 | version=version.replaceAll("\\$version", number); 123 | jtpLicense.setText(license); 124 | jtpVersion.setText(version); 125 | jtpJavaVersion.setText(javaVersion); 126 | jspLicense=new JScrollPane(jtpLicense); 127 | jspLicense.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); 128 | //Force the stupid thing to scroll to top: 129 | jtpLicense.setCaretPosition(0); 130 | 131 | btnOK.setText("OK"); 132 | btnOK.setMnemonic(KeyEvent.VK_K); 133 | } 134 | private void layout(){ 135 | GridBug gb=new GridBug(win.getContentPane()); 136 | 137 | gb.insets.left=3;gb.insets.right=3; 138 | gb.gridXY(0).weightXY(0); 139 | 140 | gb.insets.top=10; 141 | gb.insets.bottom=10; 142 | gb.weightx=1; 143 | gb.fill=GridBug.HORIZONTAL; 144 | gb.add(jtpVersion); 145 | 146 | gb.insets.top=0; 147 | gb.addY(jtpJavaVersion); 148 | 149 | gb.insets.top=0; 150 | gb.weightXY(1); 151 | gb.fill=GridBug.BOTH; 152 | gb.addY(jspLicense); 153 | 154 | gb.weightXY(0); 155 | gb.fill=GridBug.NONE; 156 | gb.insets.top=5; 157 | gb.insets.bottom=10; 158 | gb.addY(btnOK); 159 | 160 | setFont(fontOptions); 161 | } 162 | 163 | private void listen(){ 164 | Action actions=new AbstractAction() { 165 | public void actionPerformed(ActionEvent event) { 166 | win.setVisible(false); 167 | } 168 | }; 169 | btnOK.addActionListener(actions); 170 | pInfo.currentOS.fixEnterKey(btnOK, actions); 171 | KeyMapper.easyCancel(btnOK, actions); 172 | } 173 | 174 | 175 | /////////// 176 | // TEST: // 177 | /////////// 178 | 179 | public static void main(final String[] args) throws Exception { 180 | javax.swing.SwingUtilities.invokeLater(new Runnable() { 181 | public void run() { 182 | PopupTestContext ptc=new PopupTestContext(); 183 | new About(ptc.getPopupInfo(), ptc.getFontOptions()).show(); 184 | } 185 | }); 186 | } 187 | 188 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/Finder.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup; 2 | import java.util.regex.Matcher; 3 | import java.util.regex.Pattern; 4 | import java.util.regex.PatternSyntaxException; 5 | import javax.swing.AbstractAction; 6 | import javax.swing.Action; 7 | import javax.swing.JButton; 8 | import javax.swing.JCheckBox; 9 | import javax.swing.JComponent; 10 | import javax.swing.JDialog; 11 | import javax.swing.JFrame; 12 | import javax.swing.JLabel; 13 | import javax.swing.JPanel; 14 | import javax.swing.JSeparator; 15 | import javax.swing.KeyStroke; 16 | import javax.swing.SwingConstants; 17 | import javax.swing.text.Caret; 18 | import javax.swing.text.Document; 19 | import javax.swing.text.Position; 20 | import javax.swing.text.Segment; 21 | 22 | class Finder { 23 | //Results: 24 | public String replaceResult; 25 | public String lastError; 26 | 27 | private int location=-1, locationEnd=-1; 28 | 29 | //Inputs held as instance variables for convenience: 30 | private Document doc; 31 | private int offset; 32 | private boolean replaceOn; 33 | private String replaceWith; 34 | //Pattern & matcher preserved for multiple invocations: 35 | private Pattern pattern; 36 | private Matcher matcher; 37 | 38 | public int getEnd(){return locationEnd;} 39 | public int getStart() {return location;} 40 | 41 | public void reset() { 42 | pattern=null; 43 | matcher=null; 44 | } 45 | public Finder setDocument(Document doc, int offset) { 46 | this.doc=doc; 47 | this.offset=offset; 48 | return this; 49 | } 50 | public Finder setReplace(boolean replaceOn, String replaceWith) { 51 | this.replaceOn=replaceOn; 52 | this.replaceWith=replaceWith; 53 | return this; 54 | } 55 | 56 | public boolean find( 57 | String searchFor, 58 | boolean forwards, 59 | boolean caseSensitive, 60 | boolean regex, 61 | boolean multiline 62 | ) { 63 | replaceResult=null; 64 | location=-1; 65 | locationEnd=-1; 66 | 67 | String searchIn; 68 | try { 69 | searchIn=forwards 70 | ?doc.getText(offset, doc.getLength()-offset) 71 | :doc.getText(0, offset); 72 | } catch (Exception e) { 73 | throw new RuntimeException(e); 74 | } 75 | if (regex) 76 | findRegex( searchFor, searchIn, forwards, caseSensitive, multiline); 77 | else 78 | findRegular(searchFor, searchIn, forwards, caseSensitive); 79 | return location!=-1; 80 | } 81 | private void findRegular(String searchFor, String searchIn, boolean forwards, boolean caseSensitive) { 82 | if (caseSensitive) 83 | location=forwards 84 | ?searchIn.indexOf(searchFor) 85 | :searchIn.lastIndexOf(searchFor); 86 | else { 87 | //We search both low & up case because apparently 88 | //there are character sets where this is the only thing 89 | //that works. Turkish or something. Whatever. 90 | searchFor=searchFor.toLowerCase(); 91 | String searchInLow=searchIn.toLowerCase(); 92 | location=forwards ?searchInLow.indexOf(searchFor) 93 | :searchInLow.lastIndexOf(searchFor); 94 | searchFor=searchFor.toUpperCase(); 95 | String searchInHi=searchIn.toUpperCase(); 96 | int loc =forwards ?searchInHi.indexOf(searchFor) 97 | :searchInHi.lastIndexOf(searchFor); 98 | if (location==-1 || (loc!=-1 && loc alerter; 38 | 39 | // Controls: 40 | private JDialog win; 41 | private JTextField jtfRow; 42 | private JButton btnOK, btnCancel; 43 | private boolean initialized; 44 | 45 | // State: 46 | private boolean badEntry=false, cancelled=false; 47 | private int result=-1; 48 | 49 | ///////////////////// 50 | // PUBLIC METHODS: // 51 | ///////////////////// 52 | 53 | public GoToLine(PopupInfo pInfo, FontOptions fontOptions, Setter alerter) { 54 | this.pInfo=pInfo; 55 | this.fontOptions=fontOptions; 56 | this.alerter=alerter; 57 | pInfo.addFontListener(fo -> setFont(fo)); 58 | } 59 | public int show() { 60 | init(); 61 | String s=jtfRow.getText(); 62 | if (s!=null && !s.equals("")){ 63 | jtfRow.setCaretPosition(0); 64 | jtfRow.moveCaretPosition(s.length()); 65 | } 66 | win.pack(); 67 | Positioner.set(pInfo.parentFrame, win, false); 68 | 69 | result=-1; 70 | badEntry=true; 71 | cancelled=false; 72 | while (badEntry && !cancelled) 73 | doShow(); 74 | return result; 75 | } 76 | 77 | //////////////////////// 78 | // // 79 | // PRIVATE METHODS: // 80 | // // 81 | //////////////////////// 82 | 83 | private void doShow() { 84 | win.setVisible(true); 85 | win.toFront(); 86 | } 87 | 88 | /** action=true means OK, false means Cancel */ 89 | private void click(boolean action) { 90 | badEntry=false; 91 | cancelled=!action; 92 | win.setVisible(false); 93 | if (action){ 94 | try { 95 | result=Integer.parseInt(jtfRow.getText()); 96 | } catch (NumberFormatException e) { 97 | alerter.set("Value entered is not a valid number "); 98 | badEntry=true; 99 | return; 100 | } 101 | if (result<=0) { 102 | alerter.set("Value must be greater than 0"); 103 | badEntry=true; 104 | return; 105 | } 106 | } 107 | } 108 | 109 | private void setFont(FontOptions fo) { 110 | this.fontOptions=fo; 111 | if (win!=null){ 112 | fontOptions.getControlsFont().set(win); 113 | win.pack(); 114 | } 115 | } 116 | 117 | /////////////////////////// 118 | // CREATE/LAYOUT/LISTEN: // 119 | /////////////////////////// 120 | 121 | private void init() { 122 | if (!initialized) { 123 | create(); 124 | layout(); 125 | listen(); 126 | initialized=true; 127 | } 128 | } 129 | 130 | private void create(){ 131 | win=new JDialog(pInfo.parentFrame, true); 132 | win.setResizable(false); 133 | win.setTitle("Go to line"); 134 | jtfRow=new JTextField(); 135 | jtfRow.setColumns(8); 136 | btnOK =new JButton("OK"); 137 | btnOK.setMnemonic(KeyEvent.VK_K); 138 | btnCancel=new JButton("Cancel"); 139 | btnCancel.setMnemonic(KeyEvent.VK_C); 140 | } 141 | 142 | ///////////// 143 | 144 | private void layout() { 145 | GridBug gb=new GridBug(win); 146 | gb.gridy=0; 147 | gb.weightXY(0); 148 | gb.fill=GridBug.HORIZONTAL; 149 | gb.anchor=GridBug.NORTHWEST; 150 | gb.add(getInputPanel()); 151 | gb.fill=GridBug.HORIZONTAL; 152 | gb.weightXY(1); 153 | gb.addY(getButtons()); 154 | setFont(fontOptions); 155 | } 156 | private JPanel getInputPanel() { 157 | JPanel jp=new JPanel(); 158 | GridBug gb=new GridBug(jp); 159 | gb.weightXY(0).gridXY(0); 160 | gb.anchor=GridBug.WEST; 161 | 162 | gb.insets.top=2; 163 | gb.insets.bottom=2; 164 | gb.insets.left=5; 165 | 166 | JLabel label=new JLabel("Line # "); 167 | gb.add(label); 168 | gb.insets.left=0; 169 | gb.insets.right=5; 170 | gb.weightXY(1, 0).setFill(GridBug.HORIZONTAL).addX(jtfRow); 171 | 172 | return jp; 173 | } 174 | private JPanel getButtons() { 175 | JPanel panel=new JPanel(); 176 | GridBug gb=new GridBug(panel); 177 | Insets insets=gb.insets; 178 | insets.top=5; 179 | insets.bottom=5; 180 | insets.left=5; 181 | insets.right=5; 182 | 183 | gb.gridx=0; 184 | gb.add(btnOK); 185 | gb.addX(btnCancel); 186 | return panel; 187 | } 188 | private void listen() { 189 | Action okAction=new AbstractAction() { 190 | public void actionPerformed(ActionEvent event) {click(true);} 191 | }; 192 | btnOK.addActionListener(okAction); 193 | 194 | // Pressing enter anywhere means ok: 195 | KeyMapper.accel(btnOK, okAction, KeyMapper.key(KeyEvent.VK_ENTER)); 196 | 197 | Action cancelAction=new AbstractAction() { 198 | public void actionPerformed(ActionEvent event) {click(false);} 199 | }; 200 | btnCancel.addActionListener(cancelAction); 201 | KeyMapper.easyCancel(btnCancel, cancelAction); 202 | win.addWindowListener(new WindowAdapter() { 203 | public void windowClosing(WindowEvent e){ 204 | click(false); 205 | } 206 | }); 207 | } 208 | 209 | ///////////// 210 | /// TEST: /// 211 | ///////////// 212 | 213 | public static void main(final String[] args) throws Exception { 214 | javax.swing.SwingUtilities.invokeLater(()->{ 215 | try { 216 | PopupTestContext ptc=new PopupTestContext(); 217 | KAlert alerter=new KAlert(ptc.getPopupInfo(), ptc.getFontOptions()); 218 | GoToLine gtl=new GoToLine(ptc.getPopupInfo(), ptc.getFontOptions(), alerter); 219 | System.out.println(gtl.show()); 220 | } catch (Exception e) { 221 | e.printStackTrace(); 222 | } 223 | }); 224 | } 225 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/Help.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup; 2 | import java.awt.Component; 3 | import java.awt.Container; 4 | import java.awt.Dimension; 5 | import java.awt.Font; 6 | import java.awt.GridBagConstraints; 7 | import java.awt.GridBagLayout; 8 | import java.awt.Insets; 9 | import java.awt.Point; 10 | import java.awt.Rectangle; 11 | import java.awt.Window; 12 | import java.awt.event.ActionEvent; 13 | import java.awt.event.ActionListener; 14 | import java.awt.event.KeyAdapter; 15 | import java.awt.event.KeyEvent; 16 | import java.awt.event.KeyListener; 17 | import java.io.InputStream; 18 | import javax.swing.AbstractAction; 19 | import javax.swing.Action; 20 | import javax.swing.BorderFactory; 21 | import javax.swing.BoxLayout; 22 | import javax.swing.JButton; 23 | import javax.swing.JDialog; 24 | import javax.swing.JFrame; 25 | import javax.swing.JLabel; 26 | import javax.swing.JPanel; 27 | import javax.swing.JScrollPane; 28 | import javax.swing.JTextPane; 29 | import javax.swing.event.HyperlinkEvent; 30 | import javax.swing.event.HyperlinkListener; 31 | import javax.swing.ScrollPaneConstants; 32 | import org.tmotte.common.io.Loader; 33 | import org.tmotte.common.swang.CurrentOS; 34 | import org.tmotte.common.swang.GridBug; 35 | import org.tmotte.common.swang.KeyMapper; 36 | import org.tmotte.klonk.config.option.FontOptions; 37 | import org.tmotte.klonk.config.PopupInfo; 38 | 39 | public class Help { 40 | 41 | // DI: 42 | private PopupInfo pInfo; 43 | private FontOptions fontOptions; 44 | private String homeDir; 45 | 46 | // Controls: 47 | private JButton btnOK; 48 | private JDialog win; 49 | private JTextPane jtp; 50 | private JScrollPane jsp; 51 | private Container mtaContainer; 52 | 53 | // State: 54 | private boolean initialized; 55 | 56 | public Help(PopupInfo pInfo, FontOptions fontOptions, String homeDir) { 57 | this.pInfo=pInfo; 58 | this.fontOptions=fontOptions; 59 | this.homeDir=homeDir; 60 | pInfo.addFontListener(fo -> setFont(fo)); 61 | } 62 | public void show() { 63 | show(null); 64 | } 65 | public void show(Rectangle bounds) { 66 | init(); 67 | Point pt=pInfo.parentFrame.getLocation(); 68 | if (bounds!=null) 69 | win.setBounds(bounds); 70 | win.setLocation(pt.x+20, pt.y+20); 71 | win.setVisible(true); 72 | win.paintAll(win.getGraphics()); 73 | win.toFront(); 74 | } 75 | private void setFont(FontOptions fo) { 76 | this.fontOptions=fo; 77 | if (win!=null){ 78 | fontOptions.getControlsFont().set(win); 79 | win.pack(); 80 | } 81 | } 82 | 83 | 84 | //////////////////////// 85 | // PRIVATE METHODS: // 86 | //////////////////////// 87 | 88 | private void click() { 89 | win.setVisible(false); 90 | } 91 | 92 | private void init() { 93 | if (!initialized) { 94 | create(); 95 | layout(); 96 | listen(); 97 | initialized=true; 98 | } 99 | } 100 | 101 | // CREATE/LAYOUT/LISTEN: // 102 | private void create() { 103 | win=new JDialog(pInfo.parentFrame, true); 104 | 105 | jtp=new JTextPane(); 106 | jtp.setEditable(false); 107 | jtp.setBorder(null); 108 | jtp.setOpaque(false); 109 | jtp.setContentType("text/html"); 110 | 111 | jtp.addHyperlinkListener(new HyperlinkListener() { 112 | @Override public void hyperlinkUpdate(final HyperlinkEvent evt) { 113 | if (HyperlinkEvent.EventType.ACTIVATED == evt.getEventType()){ 114 | String desc = evt.getDescription(); 115 | if (desc == null || !desc.startsWith("#")) return; 116 | desc = desc.substring(1); 117 | jtp.scrollToReference(desc); 118 | } 119 | } 120 | }); 121 | 122 | String helpText=Loader.loadUTF8String(getClass(), "Help.html"); 123 | helpText=helpText.replace("$[Home]", homeDir); 124 | jtp.setText(helpText); 125 | jsp=new JScrollPane(jtp); 126 | jsp.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); 127 | jsp.getVerticalScrollBar().setUnitIncrement(16); 128 | if (pInfo.currentOS.isOSX) 129 | jsp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); 130 | 131 | //Force the stupid thing to scroll to top: 132 | jtp.setCaretPosition(0); 133 | 134 | btnOK=new JButton("OK"); 135 | } 136 | private void layout(){ 137 | GridBug gb=new GridBug(win); 138 | gb.gridy=0; 139 | gb.weightXY(1, 1); 140 | gb.fill=GridBug.BOTH; 141 | gb.add(jsp); 142 | 143 | gb.weighty=0.0; 144 | gb.insets.top=5; 145 | gb.insets.bottom=5; 146 | gb.fill=GridBug.NONE; 147 | gb.addY(btnOK); 148 | 149 | setFont(fontOptions); 150 | 151 | Rectangle rect=pInfo.parentFrame.getBounds(); 152 | rect.x+=20; rect.y+=20; 153 | rect.width=Math.max(rect.width-40, 100); 154 | rect.height=Math.max(rect.height-40, 100); 155 | win.setBounds(rect); 156 | } 157 | private void listen() { 158 | Action okAction=new AbstractAction() { 159 | public void actionPerformed(ActionEvent event) { 160 | click(); 161 | } 162 | }; 163 | btnOK.addActionListener(okAction); 164 | KeyMapper.easyCancel(btnOK, okAction); 165 | pInfo.currentOS.fixEnterKey(btnOK, okAction); 166 | jtp.addKeyListener(new KeyAdapter() { 167 | public void keyPressed(KeyEvent ke) { 168 | if (ke.getKeyCode()==KeyEvent.VK_TAB) 169 | btnOK.requestFocusInWindow(); 170 | } 171 | }); 172 | } 173 | 174 | ///////////// 175 | /// TEST: /// 176 | ///////////// 177 | 178 | public static void main(final String[] args) throws Exception{ 179 | javax.swing.SwingUtilities.invokeLater(new Runnable() { 180 | public void run() { 181 | PopupTestContext ptc=new PopupTestContext(); 182 | Help h=new Help( 183 | ptc.getPopupInfo(), ptc.getFontOptions(), "." 184 | ); 185 | h.show(new Rectangle(800,400)); 186 | } 187 | }); 188 | } 189 | 190 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/LineDelimiterListener.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup; 2 | public interface LineDelimiterListener { 3 | public void setDefault(String delimiter); 4 | public void setThis(String delimiter); 5 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/PopupTestContext.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup; 2 | import java.awt.Image; 3 | import java.awt.event.KeyAdapter; 4 | import java.awt.event.KeyEvent; 5 | import java.awt.event.KeyListener; 6 | import java.awt.event.WindowAdapter; 7 | import java.awt.event.WindowEvent; 8 | import java.awt.event.WindowListener; 9 | import java.util.List; 10 | import javax.swing.JFrame; 11 | import org.tmotte.common.swang.CurrentOS; 12 | import org.tmotte.klonk.config.msg.Setter; 13 | import org.tmotte.klonk.config.msg.StatusUpdate; 14 | import org.tmotte.klonk.config.option.FontOptions; 15 | import org.tmotte.klonk.config.KPersist; 16 | import org.tmotte.klonk.config.KHome; 17 | import org.tmotte.klonk.config.BootContext; 18 | import org.tmotte.klonk.config.PopupInfo; 19 | import org.tmotte.klonk.controller.CtrlMain; 20 | import org.tmotte.klonk.io.FileListen; 21 | import org.tmotte.klonk.io.KLog; 22 | import org.tmotte.klonk.Menus; 23 | 24 | /** 25 | * For testing popups without the overhead of the main application running. 26 | */ 27 | public class PopupTestContext { 28 | 29 | //DI Components: 30 | KHome home; 31 | KLog log; 32 | KPersist persist; 33 | JFrame mainFrame; 34 | Setter fail; 35 | PopupInfo popupInfo; 36 | FontOptions fontOptions; 37 | CurrentOS currentOS=new CurrentOS(); 38 | 39 | 40 | 41 | public KPersist getPersist() { 42 | if (persist==null) 43 | persist=new KPersist(getHome(), getFail(), currentOS); 44 | return persist; 45 | } 46 | protected KHome getHome() { 47 | if (home==null) 48 | //This could be improved by using a command line argument 49 | home=new KHome("./test/home"); 50 | return home; 51 | } 52 | public JFrame getMainFrame() { 53 | if (mainFrame==null) 54 | mainFrame=makeMainFrame(); 55 | return mainFrame; 56 | } 57 | public PopupInfo getPopupInfo() { 58 | if (popupInfo==null) 59 | popupInfo=new PopupInfo(getMainFrame(), getCurrentOS()); 60 | return popupInfo; 61 | } 62 | public FontOptions getFontOptions() { 63 | if (fontOptions==null) 64 | fontOptions=getPersist().getFontAndColors(); 65 | return fontOptions; 66 | } 67 | public Setter getFail() { 68 | if (fail==null) 69 | fail=(Throwable t)->t.printStackTrace(System.err); 70 | return fail; 71 | } 72 | public Image getPopupIcon() { 73 | return BootContext.getPopupIcon(this); 74 | } 75 | public CurrentOS getCurrentOS() { 76 | return currentOS; 77 | } 78 | 79 | //////////////// 80 | // UTILITIES: // 81 | //////////////// 82 | 83 | public static JFrame makeMainFrame() { 84 | BootContext.initLookFeel(); 85 | JFrame mainFrame=new JFrame("Klonk - Test Main Frame"); 86 | KeyAdapter ka=new KeyAdapter() { 87 | public void keyPressed(KeyEvent e){ 88 | if (e.getKeyCode()==KeyEvent.VK_ESCAPE) 89 | System.exit(0); 90 | } 91 | }; 92 | mainFrame.addKeyListener(ka); 93 | mainFrame.addWindowListener(new WindowAdapter() { 94 | public void windowClosing(WindowEvent e){ 95 | System.exit(0); 96 | } 97 | }); 98 | mainFrame.setVisible(true); 99 | mainFrame.setBounds(new java.awt.Rectangle(400,400,300,300)); 100 | mainFrame.toFront(); 101 | return mainFrame; 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/ShellCommandParser.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup; 2 | import java.util.List; 3 | import java.io.File; 4 | import java.util.ArrayList; 5 | import java.util.regex.Pattern; 6 | import org.tmotte.common.text.StringChunker; 7 | import org.tmotte.common.swang.CurrentOS; 8 | 9 | class ShellCommandParser { 10 | static Pattern delimiterPattern=Pattern.compile("(\"|'|\\p{Blank})"); 11 | static String currFileMarker="[$1]"; 12 | 13 | public static class Shex extends RuntimeException { 14 | private static final long serialVersionUID = 1L; 15 | public Shex(String msg) {super(msg);} 16 | } 17 | public static List parse(String cmd) { 18 | return parse(cmd, null); 19 | } 20 | public static List parse(String cmd, String currFileName) { 21 | List results=new ArrayList<>(); 22 | if (referencesCurrFile(cmd) & currFileName!=null) 23 | cmd=cmd.replace(currFileMarker, currFileName); 24 | 25 | //Maybe it's just one big command that references a real file, spaces or no spaces - 26 | //like C:\program files\booger hooger\foo.exe: 27 | if (new File(cmd).exists()){ 28 | results.add(cmd); 29 | return results; 30 | } 31 | 32 | //See if we can parse out an actual command that has spaces in it, and parameters 33 | //after that - because stupid people put programs in C:\Program Files: 34 | StringChunker sc=new StringChunker(cmd); 35 | boolean foundProgram=false; 36 | String execute=""; 37 | while (sc.find(" ")) { 38 | execute+=sc.getUpTo(); 39 | if (new File(execute).exists()){ 40 | results.add(execute); 41 | return getProgramArguments(results, sc); 42 | } 43 | execute+=sc.getFound(); 44 | } 45 | 46 | //If we never found a blank, this was definitely a straight command with no arguments 47 | //even though it doesn't point to an actual file, like say "ls" or "ps" 48 | if (execute.equals("")){ 49 | results.add(cmd); 50 | return results; 51 | } 52 | 53 | //OK then let's just assume the first blank ends the program, even the program doesn't seem 54 | //to exist, like "ps -aux". Then everything else is an argument, isn't it: 55 | sc.reset(cmd); 56 | results.add(sc.getUpTo(" ")); 57 | return getProgramArguments(results, sc); 58 | } 59 | 60 | private static boolean referencesCurrFile(String cmd) { 61 | return cmd.indexOf(currFileMarker)!=-1; 62 | } 63 | 64 | private static List getProgramArguments(List results, StringChunker sc){ 65 | 66 | while (sc.find(delimiterPattern)){ 67 | String found=sc.getFound(); 68 | if (found.equals("\"")){ 69 | if (!sc.find("\"")) 70 | throw new Shex("You appear to be missing a trailing \" character"); 71 | results.add(sc.getUpTo()); 72 | } 73 | else 74 | if (found.equals("'")){ 75 | if (!sc.find("'")) 76 | throw new Shex("You appear to be missing a trailing ' character"); 77 | results.add(sc.getUpTo()); 78 | } 79 | else { 80 | String r=sc.getUpTo().trim(); 81 | if (!r.equals("")) 82 | results.add(r); 83 | } 84 | } 85 | if (!sc.finished()) 86 | results.add(sc.getRest()); 87 | return results; 88 | } 89 | 90 | /////////// 91 | // TEST: // 92 | /////////// 93 | 94 | public static void main(String[] args) { 95 | String 96 | command=args[0], 97 | currFileName=args.length>1 98 | ?args[1] :null; 99 | List result=parse(args[0], currFileName); 100 | for (String s: result) 101 | System.out.println("-->"+s); 102 | } 103 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/YesNoCancelAnswer.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup; 2 | public class YesNoCancelAnswer { 3 | public final static int YES=1, NO=2, CANCEL=3; 4 | private int answer; 5 | public YesNoCancelAnswer(int answer) {this.answer=answer;} 6 | public boolean isYes(){return answer==YES;} 7 | public boolean isNo() {return answer==NO;} 8 | public boolean isCancel() {return answer==CANCEL;} 9 | public String toString() { 10 | if (isYes()) 11 | return "Yes"; 12 | else 13 | if (isNo()) 14 | return "No"; 15 | else 16 | if (isCancel()) 17 | return "Cancel"; 18 | else 19 | throw new RuntimeException("Unexpected result "+answer); 20 | } 21 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/ssh/SSHFileDialogNoFileException.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup.ssh; 2 | /** 3 | * This is so we can throw an exception to force the FileDialog to 4 | * back off and bail, then catch it and accept it as ok and be quiet 5 | * instead of barking. 6 | */ 7 | public class SSHFileDialogNoFileException extends RuntimeException { 8 | private static final long serialVersionUID = 1L; 9 | public SSHFileDialogNoFileException() { 10 | super(); 11 | } 12 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/ssh/SSHFileView.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup.ssh; 2 | import org.tmotte.klonk.ssh.SSHFile; 3 | import javax.swing.filechooser.FileView; 4 | import java.io.File; 5 | 6 | public class SSHFileView extends FileView{ 7 | 8 | public SSHFileView() { 9 | super(); 10 | } 11 | /** 12 | * For some reason this gets called even though we already have a FileSystemView class that 13 | * does the same thing. 14 | */ 15 | public Boolean isTraversable(File fdir){ 16 | if (SSHFile.cast(fdir)==null) 17 | return super.isTraversable(fdir); 18 | return fdir.isDirectory(); 19 | } 20 | 21 | 22 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/ssh/SSHOpenFromResult.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup.ssh; 2 | public class SSHOpenFromResult { 3 | public final String sshFilename; 4 | public boolean sudo=false; 5 | public SSHOpenFromResult(String f, boolean s) { 6 | this.sshFilename=f; 7 | this.sudo=s; 8 | } 9 | public String toString() { 10 | return sshFilename+ (sudo ?" SUDO" :""); 11 | } 12 | } -------------------------------------------------------------------------------- /java/org/tmotte/klonk/windows/popup/ssh/Test.java: -------------------------------------------------------------------------------- 1 | package org.tmotte.klonk.windows.popup.ssh; 2 | import java.io.File; 3 | import javax.swing.JFrame; 4 | import org.tmotte.common.swang.CurrentOS; 5 | import org.tmotte.klonk.config.msg.UserNotify; 6 | import org.tmotte.klonk.config.option.SSHOptions; 7 | import org.tmotte.klonk.ssh.SSH; 8 | import org.tmotte.klonk.ssh.SSHConnections; 9 | import org.tmotte.klonk.ssh.SSHExec; 10 | import org.tmotte.klonk.ssh.SSHFile; 11 | import org.tmotte.klonk.windows.popup.KAlert; 12 | import org.tmotte.klonk.windows.popup.PopupTestContext; 13 | 14 | class Test { 15 | 16 | public static void main(String[] args) throws Exception { 17 | String path=null; 18 | boolean forSave=false; 19 | for (int i=0; i -s"); 42 | } 43 | private static void test(final String path, final boolean forSave) throws Exception { 44 | 45 | //This is obnoxious but we have to do it when we get a null file: 46 | Thread.setDefaultUncaughtExceptionHandler( 47 | new Thread.UncaughtExceptionHandler() { 48 | public void uncaughtException(Thread t, Throwable e){ 49 | if (e instanceof SSHFileDialogNoFileException) 50 | System.err.println("OK no biggie "+e); 51 | else 52 | e.printStackTrace(); 53 | } 54 | } 55 | ); 56 | 57 | final PopupTestContext ptc=new PopupTestContext(); 58 | final SSHOptions options=ptc.getPersist().getSSHOptions(); 59 | javax.swing.SwingUtilities.invokeLater(new Runnable() { 60 | public void run() { 61 | try { 62 | UserNotify notifier=new UserNotify(System.out); 63 | KAlert alerter=new KAlert(ptc.getPopupInfo(), ptc.getFontOptions()); 64 | SSHConnections conns=new SSHConnections(notifier) 65 | .withOptions(options) 66 | .withLogin( 67 | new SSHLogin(ptc.getPopupInfo(), ptc.getFontOptions(), alerter) 68 | ); 69 | org.tmotte.klonk.windows.popup.FileDialogWrapper fdw= 70 | new org.tmotte.klonk.windows.popup.FileDialogWrapper( 71 | ptc.getPopupInfo(), 72 | new SSHFileSystemView(conns, notifier), 73 | new SSHFileView() 74 | ); 75 | { 76 | File dir=null, file=null; 77 | if (path!=null) { 78 | SSHFile temp=conns.getSSHFile(path); 79 | if (temp.isDirectory()) 80 | dir=temp; 81 | else 82 | file=temp; 83 | } 84 | File picked=fdw.show(forSave, file, dir); 85 | System.out.println("RESULT: "+ 86 | picked+" "+(picked==null ?"" :picked.getClass()) 87 | ); 88 | } 89 | } catch (Exception e) { 90 | e.printStackTrace(); 91 | } 92 | }//run() 93 | }); 94 | } 95 | } -------------------------------------------------------------------------------- /lib/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/lib/Thumbs.db -------------------------------------------------------------------------------- /lib/app-find-replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/lib/app-find-replace.png -------------------------------------------------------------------------------- /lib/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/lib/app.icns -------------------------------------------------------------------------------- /lib/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/lib/app.ico -------------------------------------------------------------------------------- /lib/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/lib/app.png -------------------------------------------------------------------------------- /lib/apptest-find-replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/lib/apptest-find-replace.png -------------------------------------------------------------------------------- /lib/apptest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/lib/apptest.png -------------------------------------------------------------------------------- /lib/classpath.sh: -------------------------------------------------------------------------------- 1 | export CLASSPATH="build"$(find lib -name '*.jar' | gawk '{printf ";"$1}') 2 | -------------------------------------------------------------------------------- /lib/config.jsmooth: -------------------------------------------------------------------------------- 1 | 2 | 3 | registry 4 | javahome 5 | jrepath 6 | jdkpath 7 | exepath 8 | jview 9 | true 10 | ..\dist\bin\klonk.exe 11 | 12 | app.ico 13 | 14 | -1 15 | ..\dist\klonk.jar 16 | 17 | 18 | 19 | Xshare 20 | off 21 | 22 | 23 | org.tmotte.klonk.config.BootContext 24 | -1 25 | 26 | 27 | Windowed Wrapper 28 | 29 | Message 30 | Java has not been found on your computer. Do you want to download it? 31 | 32 | 33 | URL 34 | http://www.java.com 35 | 36 | 37 | 38 | 39 | SingleProcess 40 | 1 41 | 42 | 43 | SingleInstance 44 | 0 45 | 46 | 47 | JniSmooth 48 | 0 49 | 50 | 56 | 57 | -------------------------------------------------------------------------------- /lib/jar/jsch-0.1.51.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/lib/jar/jsch-0.1.51.jar -------------------------------------------------------------------------------- /lib/jar/jsch-0.1.53.jar.broken: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/lib/jar/jsch-0.1.53.jar.broken -------------------------------------------------------------------------------- /lib/jar/vngx-jsch-0.10.jar.unused: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/lib/jar/vngx-jsch-0.10.jar.unused -------------------------------------------------------------------------------- /lib/makedmg: -------------------------------------------------------------------------------- 1 | # We are limiting to 98 megabytes heap space below because otherwise it grows continuously 2 | # without garbage collection. Loading large files is so slow that it probably won't make sense to 3 | # go much larger. We are meaning to add back in serial garbage collection as well, because we 4 | # used to crash every 2.5 days otherwise (see javapackager script further below). 5 | # 6 | ant clean config.prod jar 7 | 8 | jpackage --name Klonk --input dist --main-jar klonk.jar --icon lib/app.icns \ 9 | --main-class org.tmotte.klonk.config.Klonk --java-options "-Xmx128m -Xss1024k" 10 | 11 | 12 | 13 | ###################################################################################################### 14 | # This is the old javapackager that came with Java 8, and tends to break on modern JDK - dunno why 15 | # it's even included if it can't do anything but die. Keeping the script for reference point, for now: 16 | # 17 | #javapackager -deploy -native dmg -srcfiles dist/klonk.jar -outdir dist/dmg -outfile klonk \ 18 | # -appclass org.tmotte.klonk.config.Klonk -title "Klonk" -name Klonk -Bicon=lib/app.icns \ 19 | # -BjvmOptions=-Xmx128m -BjvmOptions=-XX:+UseSerialGC -BjvmOptions=-Xss1024k -Bruntime= 20 | 21 | -------------------------------------------------------------------------------- /lib/makeexe: -------------------------------------------------------------------------------- 1 | # This is not working right now. Our historic method still does. 2 | ant clean config.prod jar 3 | jpackage --name Klonk --input dist --main-jar klonk.jar --icon lib/app.icns \ 4 | --type exe \ 5 | --main-class org.tmotte.klonk.config.Klonk -------------------------------------------------------------------------------- /lib/makewin.bat: -------------------------------------------------------------------------------- 1 | rem Note: javapackager comes with java 8 distributions only 2 | rem This will advise you to install software from http://www.jrsoftware.org/, which 3 | rem is kind of lame. 4 | 5 | ant clean config.prod jar 6 | javapackager -deploy -native exe -srcfiles dist/klonk.jar -outdir dist/ -outfile klonk -appclass org.tmotte.klonk.config.Klonk -title "Klonk" -name Klonk 7 | -------------------------------------------------------------------------------- /license.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Klonk License 5 | 6 | 13 | 14 | 15 | 16 | 18 | 19 | 20 |

Klonk License

21 |

Copyright ©2013-2024 Troy Motte 22 | 23 |

Redistribution and use in source and binary forms, with or without 24 | modification, are permitted provided that the following conditions 25 | are met: 26 |

27 | 28 |
29 | 1. Redistributions of source and/or binary code must retain the above copyright 30 | notice, this list of conditions, and the following disclaimer. 31 |
32 | 33 |
34 | 2. The name "Klonk" must not be used to endorse or promote products 35 | derived from this software without prior written permission from 36 | the copyright holder or their authorized agent(s). 37 |
38 | 39 |
40 | 3. Products derived from this software may not be called "Klonk", nor 41 | may "Klonk" appear in their name, without prior written permission 42 | from the copyright holder or their authorized agent(s). 43 |
44 | 45 | 46 |

The Standard Disclaimer

47 |

48 | THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED 49 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 50 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 51 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR(S) OF KLONK 52 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 53 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 54 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 55 | USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 56 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 57 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 58 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 59 | SUCH DAMAGE. 60 |

61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd $(dirname $0)/.. 3 | #java -version 4 | ant "$@" || exit 1 5 | 6 | -------------------------------------------------------------------------------- /script/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | repo="klonk" 3 | ant_command='clean config.prod exe dist' 4 | 5 | cd $(dirname $0)/.. 6 | ant $ant_command 7 | 8 | cd ../zaboople.github.generate/lib/external-$repo 9 | echo 10 | echo "Current directory: "$(pwd) 11 | echo -n "WARNING: I am about to do an rm -rf. Is that okay? " 12 | read answer 13 | if [[ $answer == y* ]]; then 14 | rm -rf * 15 | fi 16 | 17 | echo 18 | cd ../../../$repo 19 | echo "Current directory: "$(pwd) 20 | echo -n "I am about to cp -r from here to zaboople.github.generate... Is that okay? " 21 | read answer 22 | if [[ $answer == y* ]]; then 23 | cp -r dist/site/* ../zaboople.github.generate/lib/external-$repo 24 | ant clean 25 | fi 26 | 27 | echo 28 | cd ../zaboople.github.generate 29 | echo "Current directory: "$(pwd) 30 | git status -------------------------------------------------------------------------------- /script/testAny.sh: -------------------------------------------------------------------------------- 1 | cd $(dirname $0)/.. || exit 1 2 | source lib/classpath.sh || exit 1 3 | #java -version || exit 1 4 | ant config.test compile && java -Xshare:off -Xms32m "$@" 5 | -------------------------------------------------------------------------------- /script/testApp.sh: -------------------------------------------------------------------------------- 1 | #Xshare:off is I think very important. Oh yeah let's share memory. No, thanks. 2 | cd $(dirname $0)/.. 3 | #source lib/classpath.sh 4 | ant config.test compile || exit 1 5 | java \ 6 | -Xshare:off \ 7 | -Xms6m \ 8 | -classpath 'build:lib\vngx-jsch-0.10.jar' org.tmotte.klonk.config.BootContext \ 9 | -home test/home "$@" 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /script/testAppCygwin.sh: -------------------------------------------------------------------------------- 1 | #Xshare:off is I think very important. Oh yeah let's share memory. No, thanks. 2 | cd $(dirname $0)/.. 3 | #source lib/classpath.sh 4 | ant config.test compile || exit 1 5 | java \ 6 | -Xshare:off \ 7 | -Xms6m \ 8 | -classpath 'build;lib\vngx-jsch-0.10.jar' org.tmotte.klonk.config.BootContext \ 9 | -home test/home "$@" 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /script/testLock.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | cd $(dirname $0)/.. 3 | ant config.test compile 4 | source lib/classpath.sh 5 | java -classpath build org.tmotte.klonk.io.LockTest test\dink 1 & 6 | java -classpath build org.tmotte.klonk.io.LockTest test\dink 2 & 7 | java -classpath build org.tmotte.klonk.io.LockTest test\dink 3 & 8 | java -classpath build org.tmotte.klonk.io.LockTest test\dink 4 & 9 | java -classpath build org.tmotte.klonk.io.LockTest test\dink 5 & 10 | java -classpath build org.tmotte.klonk.io.LockTest test\dink 6 & 11 | java -classpath build org.tmotte.klonk.io.LockTest test\dink 7 & 12 | java -classpath build org.tmotte.klonk.io.LockTest test\dink 8 & 13 | 14 | -------------------------------------------------------------------------------- /test/CR.txt: -------------------------------------------------------------------------------- 1 | This is CR, baby. Oh yeah it's CR. CR CR. -------------------------------------------------------------------------------- /test/CRLF.txt: -------------------------------------------------------------------------------- 1 | This is CRLF. 2 | 3 | Yes it is. 4 | 5 | OH yes. -------------------------------------------------------------------------------- /test/LF.txt: -------------------------------------------------------------------------------- 1 | This is LF. 2 | 3 | Yes it is really. 4 | 5 | It is. -------------------------------------------------------------------------------- /test/MS-UTF16-BE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/test/MS-UTF16-BE.txt -------------------------------------------------------------------------------- /test/MS-UTF16-LE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaboople/klonk/de682f1c5615c6219c9ff845f2f034e98906e9a6/test/MS-UTF16-LE.txt -------------------------------------------------------------------------------- /test/MS-UTF8.txt: -------------------------------------------------------------------------------- 1 | This is 2 | a file. 3 | ☃☃☃☃ -------------------------------------------------------------------------------- /test/autoopen/11.aaa: -------------------------------------------------------------------------------- 1 | 2 | awefawef 3 | awefawef 4 | 5 | awef awefawef -------------------------------------------------------------------------------- /test/autoopen/12.aaa: -------------------------------------------------------------------------------- 1 | 2 | 3 | THIS is file 12!!! 4 | -------------------------------------------------------------------------------- /test/autoopen/13.aaa: -------------------------------------------------------------------------------- 1 | 13 2 | -------------------------------------------------------------------------------- /test/autoopen/14.aaa: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 4 | -------------------------------------------------------------------------------- /test/autoopen/15.aaa: -------------------------------------------------------------------------------- 1 | 2 | 3 | HEY MAN THIS IS 15 -------------------------------------------------------------------------------- /test/autoopen/16.aaa: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /test/autoopen/17.aaa: -------------------------------------------------------------------------------- 1 | start java -classpath build org.tmotte.klonk.lock.LockTest test\dink 2 | start java -classpath build org.tmotte.klonk.lock.LockTest test\dink 3 | start java -classpath build org.tmotte.klonk.lock.LockTest test\dink 4 | start java -classpath build org.tmotte.klonk.lock.LockTest test\dink 5 | start java -classpath build org.tmotte.klonk.lock.LockTest test\dink 6 | -------------------------------------------------------------------------------- /test/autoopen/18.aaa: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is a.txt 5 | -------------------------------------------------------------------------------- /test/autoopen/3.aaa: -------------------------------------------------------------------------------- 1 | This is file number 3 -------------------------------------------------------------------------------- /test/autoopen/4.aaa: -------------------------------------------------------------------------------- 1 | wwwwassdsf4 2 | -------------------------------------------------------------------------------- /test/autoopen/5.aaa: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /test/autoopen/6.aaa: -------------------------------------------------------------------------------- 1 | This is number 6 2 | -------------------------------------------------------------------------------- /test/autoopen/7.aaa: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 77777 5 | -------------------------------------------------------------------------------- /test/autoopen/9.aaa: -------------------------------------------------------------------------------- 1 | 9 2 | --------------------------------------------------------------------------------