summaryrefslogtreecommitdiff
path: root/support/tlcockpit
diff options
context:
space:
mode:
Diffstat (limited to 'support/tlcockpit')
-rw-r--r--support/tlcockpit/ChangeLog1200
-rw-r--r--support/tlcockpit/LICENSE674
-rw-r--r--support/tlcockpit/NEWS57
-rw-r--r--support/tlcockpit/README.md123
-rw-r--r--support/tlcockpit/TODO13
-rw-r--r--support/tlcockpit/build.sbt-jdk1155
-rw-r--r--support/tlcockpit/build.sbt-jdk1255
-rw-r--r--support/tlcockpit/build.sbt-jdk830
-rw-r--r--support/tlcockpit/logo/tlcockpit-banner.jpgbin0 -> 36327 bytes
-rw-r--r--support/tlcockpit/logo/tlcockpit-banner.xcfbin0 -> 356935 bytes
-rw-r--r--support/tlcockpit/logo/tlcockpit.xcfbin0 -> 199863 bytes
-rw-r--r--support/tlcockpit/project/assembly.sbt1
-rw-r--r--support/tlcockpit/project/build.properties1
-rw-r--r--support/tlcockpit/project/plugins.sbt1
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-backups.jpgbin0 -> 107216 bytes
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-collections.jpgbin0 -> 136845 bytes
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-config.jpgbin0 -> 37865 bytes
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-loading.jpgbin0 -> 64630 bytes
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-menu-debug-expert.jpgbin0 -> 155627 bytes
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-packages.jpgbin0 -> 143423 bytes
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-paper.jpgbin0 -> 16959 bytes
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-pkgdetails.jpgbin0 -> 106077 bytes
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-postactions.jpgbin0 -> 55982 bytes
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-repositories.jpgbin0 -> 104751 bytes
-rw-r--r--support/tlcockpit/screenshots/tlcockpit-updates.jpgbin0 -> 194370 bytes
-rwxr-xr-xsupport/tlcockpit/scripts/tlcockpit.sh20
-rw-r--r--support/tlcockpit/src/main/resources/TLCockpit/spinner-small.gifbin0 -> 16225 bytes
-rw-r--r--support/tlcockpit/src/main/resources/TLCockpit/spinner.gifbin0 -> 60549 bytes
-rw-r--r--support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-128.jpgbin0 -> 3479 bytes
-rw-r--r--support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-48.jpgbin0 -> 1166 bytes
-rw-r--r--support/tlcockpit/src/main/resources/logback.xml11
-rw-r--r--support/tlcockpit/src/main/scala/TLCockpit/ApplicationMain.scala1643
-rw-r--r--support/tlcockpit/src/main/scala/TLCockpit/LocationDialog.scala140
-rw-r--r--support/tlcockpit/src/main/scala/TLCockpit/OptionsDialog.scala136
-rw-r--r--support/tlcockpit/src/main/scala/TLCockpit/PaperDialog.scala90
-rw-r--r--support/tlcockpit/src/main/scala/TLCockpit/PkgInfoDialog.scala174
-rw-r--r--support/tlcockpit/src/main/scala/TLCockpit/TLDisplayClasses.scala36
-rw-r--r--support/tlcockpit/src/main/scala/TLCockpit/Utils.scala32
-rw-r--r--support/tlcockpit/src/main/scala/TeXLive/JsonProtocol.scala22
-rw-r--r--support/tlcockpit/src/main/scala/TeXLive/OsTools.scala55
-rw-r--r--support/tlcockpit/src/main/scala/TeXLive/TLClasses.scala75
-rw-r--r--support/tlcockpit/src/main/scala/TeXLive/TlmgrProcess.scala130
-rw-r--r--support/tlcockpit/src/main/scala/TeXLive/package.scala9
-rw-r--r--support/tlcockpit/tlcockpit.man32
44 files changed, 4815 insertions, 0 deletions
diff --git a/support/tlcockpit/ChangeLog b/support/tlcockpit/ChangeLog
new file mode 100644
index 0000000000..662d0a6d4c
--- /dev/null
+++ b/support/tlcockpit/ChangeLog
@@ -0,0 +1,1200 @@
+2019-07-11 Norbert Preining <norbert@preining.info>
+
+ * Release 1.1 (HEAD -> master) [c62030d]
+
+2019-07-11 Norbert Preining <norbert@preining.info>
+
+ * ship both jdk8 and jdk11 jars, wrapper checks version (origin/master, origin/HEAD) [cf285a0]
+
+2019-07-11 Norbert Preining <norbert@preining.info>
+
+ * add build.sbt to ignored file [bcf2603]
+
+2019-07-11 Norbert Preining <norbert@preining.info>
+
+ * update NEWS [1b8cda4]
+
+2019-07-11 Norbert Preining <norbert@preining.info>
+
+ * bump version to 1.1 [3408f09]
+
+2019-07-11 Norbert Preining <norbert@preining.info>
+
+ * add jdk12 build file, update jdk11 [48193b7]
+
+2018-11-01 Norbert Preining <norbert@preining.info>
+
+ * add man page [6cf8607]
+
+2018-10-19 Norbert Preining <norbert@preining.info>
+
+ * add jdk11 todo items [1cbac0f]
+
+2018-10-19 Norbert Preining <norbert@preining.info>
+
+ * pass javaVersion from sbt to ApplicationMain and check for it [6598f3b]
+
+2018-10-19 Norbert Preining <norbert@preining.info>
+
+ * build.sbt for jdk8 and jdk11 [7581ca9]
+
+2018-10-19 Norbert Preining <norbert@preining.info>
+
+ * use sbt 1.2.6 [69433fd]
+
+2018-10-19 Norbert Preining <norbert@preining.info>
+
+ * fixes for HPos usage [6eb0073]
+
+2018-10-06 Norbert Preining <norbert@preining.info>
+
+ * Release 1.0 (tag: v1.0) [e71fb0e]
+
+2018-10-06 Norbert Preining <norbert@preining.info>
+
+ * add release.sh script [33a25bd]
+
+2018-10-06 Norbert Preining <norbert@preining.info>
+
+ * update jar [3633ea4]
+
+2018-10-06 Norbert Preining <norbert@preining.info>
+
+ * warn if Java > 1.8/8 as scalafx doesn't support it [98bd191]
+
+2018-10-06 Norbert Preining <norbert@preining.info>
+
+ * update jar [f4f13af]
+
+2018-10-06 Norbert Preining <norbert@preining.info>
+
+ * version 1.0 [7bc488c]
+
+2018-10-06 Norbert Preining <norbert@preining.info>
+
+ * add paragraph about java 11 problems [d89d0fd]
+
+2018-10-06 Norbert Preining <norbert@preining.info>
+
+ * bump scala, scalafx, assembly versions [594aab2]
+
+2018-09-21 Norbert Preining <norbert@preining.info>
+
+ * try fixing more ScalaFX thread problems [1c92a24]
+
+2018-09-18 Norbert Preining <norbert@preining.info>
+
+ * update NEWS [e054245]
+
+2018-09-17 Norbert Preining <norbert@preining.info>
+
+ * update jar (origin/15-cygwin-support) [daa89dd]
+
+2018-09-17 Norbert Preining <norbert@preining.info>
+
+ * fix tlmgr invocation on cygwin [787dbc0]
+
+2018-09-17 Norbert Preining <norbert@preining.info>
+
+ * fix script bug and make more portable [4f7a33e]
+
+2018-09-16 Norbert Preining <norbert@preining.info>
+
+ * update starter script [a20f69a]
+
+2018-09-16 Norbert Preining <norbert@preining.info>
+
+ * update jar [50f63be]
+
+2018-09-16 Norbert Preining <norbert@preining.info>
+
+ * fix invocation syntax on cygwin [b8eec5d]
+
+2018-09-16 Norbert Preining <norbert@preining.info>
+
+ * update jar [258080e]
+
+2018-09-16 Norbert Preining <norbert@preining.info>
+
+ * more fixes for Cygwin [4097112]
+
+2018-09-15 Norbert Preining <norbert@preining.info>
+
+ * update jar [28f0aa7]
+
+2018-09-15 Norbert Preining <norbert@preining.info>
+
+ * add support for Cygwin [ed917b3]
+
+2018-06-06 Norbert Preining <norbert@preining.info>
+
+ * don't close debug pane on new tlmgr command when it is open [807724f]
+
+2018-06-06 Norbert Preining <norbert@preining.info>
+
+ * turn closing of stdin and logging around for debugging [6c1794d]
+
+2018-06-05 Norbert Preining <norbert@preining.info>
+
+ * update jar [16be360]
+
+2018-06-05 Norbert Preining <norbert@preining.info>
+
+ * work for java version 10 style java.version string [9345446]
+
+2018-06-04 Norbert Preining <norbert@preining.info>
+
+ * update jar with latest fixes [4992d90]
+
+2018-06-02 Norbert Preining <norbert@preining.info>
+
+ * update README [a4ebfaf]
+
+2018-06-02 Norbert Preining <norbert@preining.info>
+
+ * update NEWS [e07bd2a]
+
+2018-06-02 Norbert Preining <norbert@preining.info>
+
+ * more error resistant java version check [6788dac]
+
+2018-06-02 Norbert Preining <norbert@preining.info>
+
+ * try using json4s.jackson [41a8084]
+
+2018-06-02 Norbert Preining <norbert@preining.info>
+
+ * check for Java version [b75464b]
+
+2018-04-08 Norbert Preining <norbert@preining.info>
+
+ * update jar and runner to add exec (tag: v0.9) [d8bfe81]
+
+2018-04-08 Norbert Preining <norbert@preining.info>
+
+ * release 0.9 [6636bae]
+
+2018-04-01 Norbert Preining <norbert@preining.info>
+
+ * add placeholder for empty backup tables [f20f3af]
+
+2018-03-05 Norbert Preining <norbert@preining.info>
+
+ * update update-tl scripts for new runner name [c9f9b3b]
+
+2018-03-05 Norbert Preining <norbert@preining.info>
+
+ * rename runner scripts for better TL integration [01615af]
+
+2018-02-06 Norbert Preining <norbert@preining.info>
+
+ * add news item [958ca74]
+
+2018-02-01 Norbert Preining <norbert@preining.info>
+
+ * update jar [9e9b7c2]
+
+2018-02-01 Norbert Preining <norbert@preining.info>
+
+ * update version number to 0.9dev [f87e6f0]
+
+2018-02-01 Norbert Preining <norbert@preining.info>
+
+ * make pkg info copyable as far as possible (Closes: #8) [5e2b8cd]
+
+2018-01-20 Norbert Preining <norbert@preining.info>
+
+ * bash scripting outdated ;-) (tag: v0.8) [50c9379]
+
+2018-01-20 Norbert Preining <norbert@preining.info>
+
+ * update update-tl script [e8a0862]
+
+2018-01-20 Norbert Preining <norbert@preining.info>
+
+ * update runner to pass on arguments [9a2a693]
+
+2018-01-20 Norbert Preining <norbert@preining.info>
+
+ * update jar [14f9fb3]
+
+2018-01-20 Norbert Preining <norbert@preining.info>
+
+ * update README and NEWS [d413f61]
+
+2018-01-20 Norbert Preining <norbert@preining.info>
+
+ * new screenshots [be83eaf]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * bump version number for release [cd1c2ac]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * update copyright in About [f414c4f]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * code cleanup according to IntelliJ [80a065f]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * run multiple external commands in a row [46ec6c6]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * update jar [2cf284a]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * properly massage shortdesc, remove todo [383b3fb]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * undo previous refactoring of init defs, breaks [e43fe5c]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * arggh, Java is a pain [37ea39f]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * update jar [852ba27]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * tlmgr status display rework [8f6d8fd]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * move status below panes, show currently running command [cea30d0]
+
+2018-01-19 Norbert Preining <norbert@preining.info>
+
+ * update --self hopefully working now [2ecd9a9]
+
+2018-01-18 Norbert Preining <norbert@preining.info>
+
+ * more work on restarting tlmgr [1b4b074]
+
+2018-01-18 Norbert Preining <norbert@preining.info>
+
+ * work on fixing update --self support [6521a64]
+
+2018-01-18 Norbert Preining <norbert@preining.info>
+
+ * more error handling for json parse errors etc [872aa87]
+
+2018-01-18 Norbert Preining <norbert@preining.info>
+
+ * update jar [9483e8e]
+
+2018-01-18 Norbert Preining <norbert@preining.info>
+
+ * rename package classes [f309e04]
+
+2018-01-18 Norbert Preining <norbert@preining.info>
+
+ * update NEWS [6023928]
+
+2018-01-18 Norbert Preining <norbert@preining.info>
+
+ * working version of loading db using --data [0397e46]
+
+2018-01-17 Norbert Preining <norbert@preining.info>
+
+ * further rework on speed up by using info --data [c6c233f]
+
+2018-01-17 Norbert Preining <norbert@preining.info>
+
+ * first shot a faster loading via info --data [1e37a6b]
+
+2018-01-17 Norbert Preining <norbert@preining.info>
+
+ * news and todo updates [4626c8e]
+
+2018-01-17 Norbert Preining <norbert@preining.info>
+
+ * fix logging [9a8536d]
+
+2018-01-17 Norbert Preining <norbert@preining.info>
+
+ * use pre-resized spinner to correct spacing of button [9428f4d]
+
+2018-01-17 Norbert Preining <norbert@preining.info>
+
+ * only show mirror select button if parsing worked out [9b1281f]
+
+2018-01-17 Norbert Preining <norbert@preining.info>
+
+ * add button to choose mirror in location dialog [fff6176]
+
+2018-01-12 Norbert Preining <norbert@preining.info>
+
+ * update jar [d83da06]
+
+2018-01-12 Norbert Preining <norbert@preining.info>
+
+ * update NEWS [1f9649d]
+
+2018-01-12 Norbert Preining <norbert@preining.info>
+
+ * bump version to 0.8dev [ad4446c]
+
+2018-01-12 Norbert Preining <norbert@preining.info>
+
+ * update TODO and README [125d0cb]
+
+2018-01-12 Norbert Preining <norbert@preining.info>
+
+ * Implement logging using two simple levels [b72c1da]
+
+2017-11-28 Norbert Preining <norbert@preining.info>
+
+ * lots of debug statements [8023e0d]
+
+2018-01-12 Norbert Preining <norbert@preining.info>
+
+ * add header to all files, update copyright [9ef778f]
+
+2017-12-08 Norbert Preining <norbert@preining.info>
+
+ * protect against missing ctan-mirrors.pl or parse errors [22881db]
+
+2017-12-08 Norbert Preining <norbert@preining.info>
+
+ * function to parse ctan-mirrors.pl via json [44dd774]
+
+2017-12-08 Norbert Preining <norbert@preining.info>
+
+ * nicer font for placeholder [357e654]
+
+2017-12-06 Norbert Preining <norbert@preining.info>
+
+ * only load upd/bck once [fb4f944]
+
+2017-12-06 Norbert Preining <norbert@preining.info>
+
+ * add spinner to all loading screens [43230d9]
+
+2017-12-06 Norbert Preining <norbert@preining.info>
+
+ * implement spinner for postaction on updates [405235d]
+
+2017-12-06 Norbert Preining <norbert@preining.info>
+
+ * add a list of TODOs [8962d76]
+
+2017-12-06 Norbert Preining <norbert@preining.info>
+
+ * add a spinner gif [a4b9811]
+
+2017-11-23 Norbert Preining <norbert@preining.info>
+
+ * update for release (tag: v0.7) [21b8d9f]
+
+2017-11-23 Norbert Preining <norbert@preining.info>
+
+ * update jar [b74de47]
+
+2017-11-23 Norbert Preining <norbert@preining.info>
+
+ * update banner image source and image [9e8677c]
+
+2017-11-22 Norbert Preining <norbert@preining.info>
+
+ * output exception on updfun errors [2de49dd]
+
+2017-11-20 Norbert Preining <norbert@preining.info>
+
+ * add tooltip for autobackup [f452a70]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * update jar [d1d5039]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * trying to fix updates and scalafx bugs [df4f202]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * update table context menu install/remove/update en/disable [51539e3]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * simplify restore routine [4086aa5]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * update NEWS [cdba808]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * check tlmgr revision and bail out if too old [66fb51b]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * change some var to val [4c950c9]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * dont need spash screen anymore [5cef8f5]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * remove already fixed todo [d690343]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * remove hopeless todo [964008b]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * fix missing texlive.infra sub packages [7d01d59]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * line by line updates for removal (needs new tlmgr) [df6a441]
+
+2017-11-17 Norbert Preining <norbert@preining.info>
+
+ * remove relocated, work on remove line updates [e143ae5]
+
+2017-11-16 Norbert Preining <norbert@preining.info>
+
+ * line updates: for install, simplify, unify [b1e8a98]
+
+2017-11-16 Norbert Preining <norbert@preining.info>
+
+ * define default stdout/err line outputfuncs and use them [bde1ad8]
+
+2017-11-16 Norbert Preining <norbert@preining.info>
+
+ * resize location edit dialog when rows are added [4846291]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * refactoring and cleanup [04c5cff]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * update jar [7b6bfab]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * update NEWS [bb06253]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * update screenshots with search functionality [19a28ef]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * search also in shortdesc, case ins (Issue: #4) [a67bafc]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * working search in package names [dc488e5]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * start working on search facility [eb8687a]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * update screenshots [47025f3]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * set initial stage size [d47569b]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * deal with "remove on server" cases [46402ea]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * deal with dying tlmgr [d057318]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * nicer placeholders while loading [18916eb]
+
+2017-11-15 Norbert Preining <norbert@preining.info>
+
+ * update todos for some misbehaviour [c705c55]
+
+2017-11-14 Norbert Preining <norbert@preining.info>
+
+ * location multi repo editing support [c14d913]
+
+2017-11-14 Norbert Preining <norbert@preining.info>
+
+ * update news [c19379f]
+
+2017-11-14 Norbert Preining <norbert@preining.info>
+
+ * forgot missing files [7d87e0d]
+
+2017-11-14 Norbert Preining <norbert@preining.info>
+
+ * more or less workable general options dialog [4e2394f]
+
+2017-11-14 Norbert Preining <norbert@preining.info>
+
+ * initial work on options display [df1e64c]
+
+2017-11-14 Norbert Preining <norbert@preining.info>
+
+ * refactor classes for strict separation between Display and TL internal [5ce8570]
+
+2017-11-14 Norbert Preining <norbert@preining.info>
+
+ * paper conf dialog working [b984cfa]
+
+2017-11-14 Norbert Preining <norbert@preining.info>
+
+ * paper conf dialog - WIP [c577f0e]
+
+2017-11-11 Norbert Preining <norbert@preining.info>
+
+ * update jar [d543dc8]
+
+2017-11-11 Norbert Preining <norbert@preining.info>
+
+ * todos [e82188e]
+
+2017-11-11 Norbert Preining <norbert@preining.info>
+
+ * activate restore functionality [9001e9b]
+
+2017-11-11 Norbert Preining <norbert@preining.info>
+
+ * support update option --no-auto-? and --reinstall... [34f4383]
+
+2017-11-11 Norbert Preining <norbert@preining.info>
+
+ * pane depending menus [8b6eb61]
+
+2017-11-11 Norbert Preining <norbert@preining.info>
+
+ * pass -d to tlmgr for now ;-) [b27678c]
+
+2017-11-10 Norbert Preining <norbert@preining.info>
+
+ * get enable/disable correct by binding on change items [2fd7e55]
+
+2017-11-10 Norbert Preining <norbert@preining.info>
+
+ * revert enable/disable change - needs more work [02d6c85]
+
+2017-11-09 Norbert Preining <norbert@preining.info>
+
+ * update jar [bed3013]
+
+2017-11-09 Norbert Preining <norbert@preining.info>
+
+ * use json output for backups parsing [6671010]
+
+2017-11-08 Norbert Preining <norbert@preining.info>
+
+ * NEWS and README update [6770851]
+
+2017-11-08 Norbert Preining <norbert@preining.info>
+
+ * new logfield, stderr -> logfield, program errors to errorfield [cd5bc1b]
+
+2017-11-08 Norbert Preining <norbert@preining.info>
+
+ * log stderr to error pane, open automatically if not empty [2bb2036]
+
+2017-11-08 Norbert Preining <norbert@preining.info>
+
+ * cleanup imports [c2aed16]
+
+2017-11-08 Norbert Preining <norbert@preining.info>
+
+ * implement pending job queue for later processing [6c9c558]
+
+2017-11-08 Norbert Preining <norbert@preining.info>
+
+ * reduce global vars [80555be]
+
+2017-11-08 Norbert Preining <norbert@preining.info>
+
+ * make sure not race condition on tlmgrLastOutput [93955ea]
+
+2017-11-08 Norbert Preining <norbert@preining.info>
+
+ * working new process management [45f9646]
+
+2017-11-07 Norbert Preining <norbert@preining.info>
+
+ * start reworking tlmgr process treatment [30fe7cd]
+
+2017-10-31 Norbert Preining <norbert@preining.info>
+
+ * fixed start command on windows [39868aa]
+
+2017-10-30 Norbert Preining <norbert@preining.info>
+
+ * fix errors on installation/removal [0514b88]
+
+2017-10-30 Norbert Preining <norbert@preining.info>
+
+ * update jar [9d476e9]
+
+2017-10-30 Norbert Preining <norbert@preining.info>
+
+ * bump version number, release needs to wait for tlmgr push [3856824]
+
+2017-10-30 Norbert Preining <norbert@preining.info>
+
+ * Merge branch 'json' [a46487e]
+
+2017-10-30 Norbert Preining <norbert@preining.info>
+
+ * todo and formatting [cd848ab]
+
+2017-10-27 Norbert Preining <norbert@preining.info>
+
+ * debug code [faec705]
+
+2017-10-27 Norbert Preining <norbert@preining.info>
+
+ * fix size constraints for pkginfo list view after spacing removed [0fcbc3f]
+
+2017-10-27 Norbert Preining <norbert@preining.info>
+
+ * tight line spacing in pkg info window [92e59c5]
+
+2017-10-27 Norbert Preining <norbert@preining.info>
+
+ * update jar [705a528]
+
+2017-10-27 Norbert Preining <norbert@preining.info>
+
+ * clean up imports [e941d92]
+
+2017-10-27 Norbert Preining <norbert@preining.info>
+
+ * clean up build.sbt [07f588a]
+
+2017-10-27 Norbert Preining <norbert@preining.info>
+
+ * better JSON parser based on case classes and new JSON format [208155d]
+
+2017-10-26 Norbert Preining <norbert@preining.info>
+
+ * update jar [08d0165]
+
+2017-10-24 Norbert Preining <norbert@preining.info>
+
+ * try to properly close on Windows, too [6464610]
+
+2017-10-25 Norbert Preining <norbert@preining.info>
+
+ * update jar [6e2936d]
+
+2017-10-25 Norbert Preining <norbert@preining.info>
+
+ * update build with new version number [6fd030b]
+
+2017-10-25 Norbert Preining <norbert@preining.info>
+
+ * add more TODOs [1583d09]
+
+2017-10-24 Norbert Preining <norbert@preining.info>
+
+ * sort kids when displaying on collections [039ef2d]
+
+2017-10-24 Norbert Preining <norbert@preining.info>
+
+ * don't display collection deps as sub-packages [b73aaf4]
+
+2017-10-24 Norbert Preining <norbert@preining.info>
+
+ * display bin deps also below collections [7aebef6]
+
+2017-10-24 Norbert Preining <norbert@preining.info>
+
+ * add NEWS entry for Windows close issue (Issue: 2) [0f0b005]
+
+2017-10-24 Norbert Preining <norbert@preining.info>
+
+ * update jar [004e5be]
+
+2017-10-24 Norbert Preining <norbert@preining.info>
+
+ * try to properly close on Windows, too [8b979e8]
+
+2017-10-23 Norbert Preining <norbert@preining.info>
+
+ * view by collections finished [f11da38]
+
+2017-10-23 Norbert Preining <norbert@preining.info>
+
+ * preparation for order by collection [08527de]
+
+2017-10-23 Norbert Preining <norbert@preining.info>
+
+ * factor out view code [44bd080]
+
+2017-10-17 Norbert Preining <norbert@preining.info>
+
+ * refactor pkg info dialog into its own class [ae8b8e1]
+
+2017-10-16 Norbert Preining <norbert@preining.info>
+
+ * format, size adaptions [f339700]
+
+2017-10-16 Norbert Preining <norbert@preining.info>
+
+ * support sizes in TLPackage and info display [90eff83]
+
+2017-10-16 Norbert Preining <norbert@preining.info>
+
+ * working version based on latest tlmgr (unreleased) [b7b5b5b]
+
+2017-10-13 Norbert Preining <norbert@preining.info>
+
+ * ignore scala worksheets from git, they are for experiments [afadf5e]
+
+2017-10-13 Norbert Preining <norbert@preining.info>
+
+ * add spray-json [889ae20]
+
+2017-10-12 Norbert Preining <norbert@preining.info>
+
+ * update jar (tag: v0.6) [c3ab9cf]
+
+2017-10-12 Norbert Preining <norbert@preining.info>
+
+ * use lable in vbox if <= 5 files [1cd41af]
+
+2017-10-12 Norbert Preining <norbert@preining.info>
+
+ * update jar [0c84e84]
+
+2017-10-12 Norbert Preining <norbert@preining.info>
+
+ * update NEWS file [0fbf74d]
+
+2017-10-12 Norbert Preining <norbert@preining.info>
+
+ * ListView for file lists in pkg info [ae78845]
+
+2017-10-11 Norbert Preining <norbert@preining.info>
+
+ * parse aways more info lines [fbc569e]
+
+2017-10-11 Norbert Preining <norbert@preining.info>
+
+ * reorder pkg info files sections, add todos [3432198]
+
+2017-10-11 Norbert Preining <norbert@preining.info>
+
+ * fix always active update all button [71f3714]
+
+2017-10-11 Norbert Preining <norbert@preining.info>
+
+ * fix mixed install state for uninstalled packages [952bc65]
+
+2017-10-11 Norbert Preining <norbert@preining.info>
+
+ * update jar [a2e61dc]
+
+2017-10-11 Norbert Preining <norbert@preining.info>
+
+ * new version [00cc785]
+
+2017-10-11 Norbert Preining <norbert@preining.info>
+
+ * infra updates: properly do --self and restart tlmgr [a791f51]
+
+2017-10-10 Norbert Preining <norbert@preining.info>
+
+ * infra updates: gives warning and disable other updates [250e40f]
+
+2017-10-09 Norbert Preining <norbert@preining.info>
+
+ * update tl update script (tag: v0.5) [4ce8185]
+
+2017-10-09 Norbert Preining <norbert@preining.info>
+
+ * update screenshot [51eb630]
+
+2017-10-09 Norbert Preining <norbert@preining.info>
+
+ * update jar [dd3cda4]
+
+2017-10-09 Norbert Preining <norbert@preining.info>
+
+ * bump version, add NEWS [b24b228]
+
+2017-10-09 Norbert Preining <norbert@preining.info>
+
+ * doc files opening improvements [e907fce]
+
+2017-10-06 Norbert Preining <norbert@preining.info>
+
+ * pre/post update status indication [bf254c5]
+
+2017-10-05 Norbert Preining <norbert@preining.info>
+
+ * todo update and cleanup/style [b5971ca]
+
+2017-10-04 Norbert Preining <norbert@preining.info>
+
+ * update jar [dfb5056]
+
+2017-10-04 Norbert Preining <norbert@preining.info>
+
+ * display list of files in pkg info dialog [e388968]
+
+2017-10-04 Norbert Preining <norbert@preining.info>
+
+ * update jar (tag: v0.4) [9a58149]
+
+2017-10-04 Norbert Preining <norbert@preining.info>
+
+ * buffered output available, use for fmtutil-sys --all [6a34ffa]
+
+2017-10-03 Norbert Preining <norbert@preining.info>
+
+ * update jar [3ef37ec]
+
+2017-10-03 Norbert Preining <norbert@preining.info>
+
+ * line by line update working [ebe75c2]
+
+2017-10-03 Norbert Preining <norbert@preining.info>
+
+ * error debugging, fixed problem [b96961f]
+
+2017-10-03 Norbert Preining <norbert@preining.info>
+
+ * work on line by line updates, broken [e1a1b8a]
+
+2017-10-03 Norbert Preining <norbert@preining.info>
+
+ * convert pkgs to observable, only do onEvent for root updates [ef5431e]
+
+2017-10-03 Norbert Preining <norbert@preining.info>
+
+ * convert backups to observable [debca79]
+
+2017-10-03 Norbert Preining <norbert@preining.info>
+
+ * add updline facitlity, switch to observablemap for updates [4025ee3]
+
+2017-10-01 Norbert Preining <norbert@preining.info>
+
+ * update jar [6abc4b7]
+
+2017-10-01 Norbert Preining <norbert@preining.info>
+
+ * switch to Map for pkgs, get shortdesc from there in updates [845fd45]
+
+2017-10-01 Norbert Preining <norbert@preining.info>
+
+ * update of update_{self,all} from populate callback [f90d683]
+
+2017-10-01 Norbert Preining <norbert@preining.info>
+
+ * async loading of update packages from update --list output [d0ebb91]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * final for now [725babe]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * more [72ea12d]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * next try [2b03768]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * try displaying an image on the README [e632260]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * fix script not to install icons to TL [bdab981]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * update jar [13ff19a]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * fix gui update on wrong thread [873f29a]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * update jar [5b26c00]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * add logo and display it [38ba77d]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * update jar [d4c50fe]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * cleanup stuff [88b686c]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * react to Enter on cmdline, center items [eeb7bfb]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * update screenshots [e164582]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * update jar [4825c16]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * use version from build.sbt in about display [9c66d68]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * only call restore once to populate backup table [a4c4753]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * protect from parallel sending of tlmgr commands [7f0112d]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * backup tab [e38e412]
+
+2017-09-28 Norbert Preining <norbert@preining.info>
+
+ * use TreeTableView, cleanup [95d64bf]
+
+2017-09-27 Norbert Preining <norbert@preining.info>
+
+ * convert to case class with Properties [59e9cbd]
+
+2017-09-27 Norbert Preining <norbert@preining.info>
+
+ * simplification of display, width adjustment [207d936]
+
+2017-09-27 Norbert Preining <norbert@preining.info>
+
+ * update jar [83b5467]
+
+2017-09-27 Norbert Preining <norbert@preining.info>
+
+ * more work [becfd1b]
+
+2017-09-27 Norbert Preining <norbert@preining.info>
+
+ * more work [9c666a7]
+
+2017-09-27 Norbert Preining <norbert@preining.info>
+
+ * update jar [66f6d37]
+
+2017-09-27 Norbert Preining <norbert@preining.info>
+
+ * simplification of menus [dba71f1]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * more layout games to get closer to TLUtil [61d886d]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * use StringProperties because Int/BooleanProperties don't work for me [4b152a8]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * include more info in TLPackage objects [8f80e18]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * reduce menu clutter, no switching of repos [183fa04]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * load remote repo on startup [a96601c]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * fix in TL update script [75ae580]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * update jar [1d8cfdb]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * use ObservableBuffer[String] for updates of stdout/stderr [216f726]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * update-tl scripts updated [b715c26]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * update jar [c2b2531]
+
+2017-09-26 Norbert Preining <norbert@preining.info>
+
+ * switch to Future/async tlmgr calls [93b554c]
+
+2017-09-05 Norbert Preining <norbert@preining.info>
+
+ * update to latest version [f21936f]
+
+2017-09-01 Norbert Preining <norbert@preining.info>
+
+ * clean up and reshuffle [4957bb5]
+
+2017-09-01 Norbert Preining <norbert@preining.info>
+
+ * latest jar [757eeb2]
+
+2017-09-01 Norbert Preining <norbert@preining.info>
+
+ * update README [e654e55]
+
+2017-09-01 Norbert Preining <norbert@preining.info>
+
+ * adjust output of assembly to go directly into jar/tlcockpit.jar [b42bd9f]
+
+2017-09-01 Norbert Preining <norbert@preining.info>
+
+ * comment unused classes [e4e03af]
+
+2017-09-01 Norbert Preining <norbert@preining.info>
+
+ * add a stopApp function to shut down tlmgr [c94d3b1]
+
+2017-09-01 Norbert Preining <norbert@preining.info>
+
+ * update jar [0df3924]
+
+2017-09-01 Norbert Preining <norbert@preining.info>
+
+ * open err tab on errors, clear on new command [e58aaee]
+
+2017-09-01 Norbert Preining <norbert@preining.info>
+
+ * context menu, open error tab automatically [343839b]
+
+2017-09-01 Norbert Preining <norbert@preining.info>
+
+ * Disable/enable update buttons according to availability of updates [23c6704]
+
+2017-08-31 Norbert Preining <norbert@preining.info>
+
+ * update jar [ef22d00]
+
+2017-08-31 Norbert Preining <norbert@preining.info>
+
+ * Allow table of packages to shrink [b51a26c]
+
+2017-08-30 Norbert Preining <norbert@preining.info>
+
+ * style cleanup [2adbc03]
+
+2017-08-30 Norbert Preining <norbert@preining.info>
+
+ * update jar file after code refactoring [a329a05]
+
+2017-08-30 Norbert Preining <norbert@preining.info>
+
+ * refactor code [5bb39d8]
+
+2017-08-30 Norbert Preining <norbert@preining.info>
+
+ * TL inclusion script update [e8376fc]
+
+2017-08-30 Norbert Preining <norbert@preining.info>
+
+ * update tlcockpit.jar [290223e]
+
+2017-08-30 Norbert Preining <norbert@preining.info>
+
+ * clean up build.sbt [c0da074]
+
+2017-08-30 Norbert Preining <norbert@preining.info>
+
+ * test mode with --test, call tlmgr.bat on Windows [4739455]
+
+2017-08-30 Norbert Preining <norbert@preining.info>
+
+ * explain requirements in readme [5da2716]
+
+2017-08-29 Norbert Preining <norbert@preining.info>
+
+ * debugging for deploy building [481d58b]
+
+2017-08-25 Norbert Preining <norbert@preining.info>
+
+ * fix jar link [1dea7a2]
+
+2017-08-25 Norbert Preining <norbert@preining.info>
+
+ * add link to jar and how to run [ee89d48]
+
+2017-08-25 Norbert Preining <norbert@preining.info>
+
+ * add prebuild jar file [58c5402]
+
+2017-08-25 Norbert Preining <norbert@preining.info>
+
+ * update README [d1ce444]
+
+2017-08-25 Norbert Preining <norbert@preining.info>
+
+ * README addons [a1469f6]
+
+2017-08-25 Norbert Preining <norbert@preining.info>
+
+ * better readme [8a84a61]
+
+2017-08-25 Norbert Preining <norbert@preining.info>
+
+ * add deployment and bin wrapper script [d12864a]
+
+2017-08-25 Norbert Preining <norbert@preining.info>
+
+ * continue renaming [29338ca]
+
+2017-08-25 Norbert Preining <norbert@preining.info>
+
+ * fix name of project in build.sbt [90a0fd3]
+
+2017-08-25 Norbert Preining <norbert@preining.info>
+
+ * working scalafx gui and some features [605f516]
+
+2017-08-15 Norbert Preining <norbert@preining.info>
+
+ * add README.md [168bac3]
+
+2017-08-15 Norbert Preining <norbert@preining.info>
+
+ * add .gitignore [1323a6d]
+
+2017-08-15 Norbert Preining <norbert@preining.info>
+
+ * first running version [c3c20af]
+
+2017-08-15 Norbert Preining <preining@logic.at>
+
+ * Initial commit [25f88ed]
+
diff --git a/support/tlcockpit/LICENSE b/support/tlcockpit/LICENSE
new file mode 100644
index 0000000000..9cecc1d466
--- /dev/null
+++ b/support/tlcockpit/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {one line to give the program's name and a brief idea of what it does.}
+ Copyright (C) {year} {name of author}
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ {project} Copyright (C) {year} {fullname}
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/support/tlcockpit/NEWS b/support/tlcockpit/NEWS
new file mode 100644
index 0000000000..e84482445f
--- /dev/null
+++ b/support/tlcockpit/NEWS
@@ -0,0 +1,57 @@
+NEWS for tlcockpit
+
+version 1.1 (20190711)
+----------------------
+- build jdk8 and jdk11 versions - jdk11 includes javafx
+ default is jdk11 version, but Unix starter script tries
+ to deduce the correct version.
+ On Windows JDK11 or above is expected
+
+version 1.0 (20181006)
+----------------------
+- check for used Java version and abort if less than < 1.8 (Java 8)
+- add support for Cygwin
+
+version 0.9 (20180408)
+----------------------
+- make package info dialog content copyable (as far as possible)
+- add placeholder for empty backup table
+
+version 0.8 (20180120)
+----------------------
+- add debug facility (-d and -dd)
+- nice spinner while waiting for loading of db
+- select mirrors for main repository facility added
+- speed up initial loading by using info --data and load
+ pkg info on demand using info --json
+- (hopefully) fixed self updated of tlmgr
+- move status indicator below the package pane, and add currently
+ running command indication
+
+version 0.7 (20171124)
+----------------------
+requires tlmgr >= 2017-11-23 (version 45888 of texlive.infra)
+- fix behavior when closing using the Window Manager X on Windows (Issue: 2)
+- use tlmgr json output
+- allow ordering packages by name and by collection
+- log and error output separated (Debug area)
+- support for paper configuration and general options
+- deal with dying tlmgr
+- add search capability (searches package names and sortdesc)
+- check whether tlmgr is new enough
+- updates, installs, removes are online updated during operation
+
+version 0.6 (20171012)
+----------------------
+- texlive infra updates handled properly (restarting tlmgr)
+- scrollable list of files in pkg info window
+
+version 0.5 (20171009)
+----------------------
+- better pkg info display
+- try to open doc files from pkg info window
+- dynamic updates of the update screen during updates
+
+version 0.4 (20171004)
+----------------------
+First half-way usable version
diff --git a/support/tlcockpit/README.md b/support/tlcockpit/README.md
new file mode 100644
index 0000000000..c5b9bbd8d2
--- /dev/null
+++ b/support/tlcockpit/README.md
@@ -0,0 +1,123 @@
+TLCockpit - GUI frontend to tlmgr
+=================================
+
+<img align="right" width="100" height="100" src="src/main/resources/TLCockpit/tlcockpit-128.jpg">
+
+TLCockpit aims at being a GUI for [tlmgr](https://www.tug.org/texlive/tlmgr.html),
+the [TeX Live](https://www.tug.org/texlive/) Manager, with a modern look and feel.
+We take inspiration from the [TeX Live Utility](http://amaxwell.github.io/tlutility/).
+
+
+Installing/Running
+------------------
+
+Since TLCockpit is included in TeX Live, it will be probably already installed
+on your system. In case it isn't, a simple call to `tlmgr install tlcockpit`
+should suffice.
+
+We are providing new jar files on the github page, but update
+the copy in TeX Live only at certain intervals. If you want to try out the latest
+and greatest, get [tlcockpit.jar](jar/tlcockpit.jar) and put it into
+`TEXLIVEROOT/texmf-dist/scripts/tlcockpit` replacing the current `tlcockpit.jar`.
+
+After this you can run it by simply typing `tlcockpit`, or via a direct `java`
+call as in `java -jar PATH/TO/tlcockpit.jar`.
+
+Debugging can be enabled by passing the option `-d` or, for more detailed debugging `-dd`.
+
+Requirements
+------------
+You need at least `tlmgr` from after 2017-11-23, in TeX Live language at least
+version 45888 or `texlive.infra`.
+
+A recent Java (at least Java 8) installation that includes JavaFX is required. If you see
+errors like
+```
+Error: A JNI error has occurred, please check your installation and try again
+Exception in thread "main" java.lang.NoClassDefFoundError: javafx/event/EventTarget
+ at java.lang.ClassLoader.defineClass1(Native Method)
+ at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
+...
+```
+that means that the Java installation does not provides JavaFX.
+
+On Windows the latest JRE from Oracle include JavaFX. On Linux one might
+need to install some extra packages (Debian/Ubuntu: openjfx).
+
+With recent release of Java 11 problems are getting worse, since JavaFX is now
+separately packaged, and ScalaFX only supports (by now) JavaFX8. That means, for
+now it is necessary to have a Java 8 installation. As soon as ScalaFX supports
+JavaFX11 we will move to use it.
+
+
+Development
+-----------
+
+Development is done in Scala with the ScalaFX widget set. The project is
+mainly a challenge for myself to learn Scala. Building is done with *sbt*,
+opening the project in IntelliJ works fine.
+
+Deployment is done via [sbt assembly](https://github.com/sbt/sbt-assembly)
+which packs all the necessary libraries into one jar file.
+
+Contributions are welcome, improvements are easily done as I just started
+writing Scala and my style is horrible ;-)
+
+Screenshots
+-----------
+
+Starting up TLCockpit
+
+![Loading screen](screenshots/tlcockpit-loading.jpg)
+
+Package listing
+
+![Package listing view](screenshots/tlcockpit-packages.jpg)
+
+View packages by collections
+
+![Packages by collections](screenshots/tlcockpit-collections.jpg)
+
+Updates listing, with one package being updated
+
+![Updates listing view](screenshots/tlcockpit-updates.jpg)
+
+After updating all packages during the run of postactions like `mktexlsr`
+
+![Postaction view](screenshots/tlcockpit-postactions.jpg)
+
+Backup/Restore listing
+
+![Backup listing view](screenshots/tlcockpit-backups.jpg)
+
+Package details
+
+![Package details](screenshots/tlcockpit-pkgdetails.jpg)
+
+Configuration dialog
+
+![Config screen](screenshots/tlcockpit-config.jpg)
+
+Repository seup dialog with dropdown for mirror selection
+
+![Repository screen](screenshots/tlcockpit-repositories.jpg)
+
+Paper setting dialog
+
+![Config screen](screenshots/tlcockpit-paper.jpg)
+
+The Tools menu, as well as the expert and debug panes open
+
+![Menu, Expert, Debug](screenshots/tlcockpit-menu-debug-expert.jpg)
+
+License
+-------
+
+GPL3+
+
+
+Author
+------
+
+Norbert Preining
+
diff --git a/support/tlcockpit/TODO b/support/tlcockpit/TODO
new file mode 100644
index 0000000000..74b5fe3336
--- /dev/null
+++ b/support/tlcockpit/TODO
@@ -0,0 +1,13 @@
+TODO
+====
+
+JDK11 support
+-------------
+
+* sbt build and assembly works
+ if we do NOT include javafx in the fat jar (see build.sbt) then
+ we the user needs javafx installed, we need to search for it,
+ and add it to the java call:
+ java -p /usr/share/openjfx/lib/ --add-modules javafx.controls,javafx.web -jar jar/tlcockpit-jdk11.jar
+
+
diff --git a/support/tlcockpit/build.sbt-jdk11 b/support/tlcockpit/build.sbt-jdk11
new file mode 100644
index 0000000000..bf490132f5
--- /dev/null
+++ b/support/tlcockpit/build.sbt-jdk11
@@ -0,0 +1,55 @@
+name := "tlcockpit"
+
+version := "1.1"
+
+scalaVersion := "2.12.7"
+
+val javaVersion:Int = 11
+
+libraryDependencies += "io.spray" %% "spray-json" % "1.3.3"
+libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2"
+libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
+libraryDependencies += "org.json4s" %% "json4s-native" % "3.6.0-M3"
+libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.6.0-M3"
+
+// Add dependency on ScalaFX library
+// libraryDependencies += "org.scalafx" %% "scalafx" % "11-R16"
+libraryDependencies += "org.scalafx" % "scalafx_2.12" % "12.0.1-R17"
+
+lazy val root = (project in file(".")).
+ enablePlugins(BuildInfoPlugin).
+ settings(
+ buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion, BuildInfoKey(("javaVersion", javaVersion))),
+ buildInfoPackage := "TLCockpit"
+ )
+
+
+// Determine OS version of JavaFX binaries
+lazy val osName = System.getProperty("os.name") match {
+ case n if n.startsWith("Linux") => "linux"
+ case n if n.startsWith("Mac") => "mac"
+ case n if n.startsWith("Windows") => "win"
+ case _ => throw new Exception("Unknown platform!")
+}
+
+lazy val javaFXModules = Seq("base", "controls", "fxml", "graphics", "media", "swing", "web")
+libraryDependencies ++= javaFXModules.map( m =>
+ // with this the modules will NOT be included in the fat jar!
+ // "org.openjfx" % s"javafx-$m" % javaVersion % "provided" classifier osName
+ "org.openjfx" % s"javafx-$m" % javaVersion.toString classifier osName
+)
+
+assemblyMergeStrategy in assembly := {
+ case PathList("module-info.class", xs @ _*) => MergeStrategy.discard
+ case PathList("META-INF", "MANIFEST.MF", xs @ _*) => MergeStrategy.discard
+ case x => MergeStrategy.first
+}
+
+mainClass in assembly := Some("TLCockpit.ApplicationMain")
+
+assemblyJarName in assembly := "tlcockpit-jdk11.jar"
+assemblyOutputPath in assembly := file("jar/tlcockpit-jdk11.jar")
+
+
+// for scalafx
+fork := true
diff --git a/support/tlcockpit/build.sbt-jdk12 b/support/tlcockpit/build.sbt-jdk12
new file mode 100644
index 0000000000..0e31eb3423
--- /dev/null
+++ b/support/tlcockpit/build.sbt-jdk12
@@ -0,0 +1,55 @@
+name := "tlcockpit"
+
+version := "1.1"
+
+scalaVersion := "2.12.7"
+
+val javaVersion:Int = 12
+
+libraryDependencies += "io.spray" %% "spray-json" % "1.3.3"
+libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2"
+libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
+libraryDependencies += "org.json4s" %% "json4s-native" % "3.6.0-M3"
+libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.6.0-M3"
+
+// Add dependency on ScalaFX library
+// libraryDependencies += "org.scalafx" %% "scalafx" % "11-R16"
+libraryDependencies += "org.scalafx" % "scalafx_2.12" % "12.0.1-R17"
+
+lazy val root = (project in file(".")).
+ enablePlugins(BuildInfoPlugin).
+ settings(
+ buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion, BuildInfoKey(("javaVersion", javaVersion))),
+ buildInfoPackage := "TLCockpit"
+ )
+
+
+// Determine OS version of JavaFX binaries
+lazy val osName = System.getProperty("os.name") match {
+ case n if n.startsWith("Linux") => "linux"
+ case n if n.startsWith("Mac") => "mac"
+ case n if n.startsWith("Windows") => "win"
+ case _ => throw new Exception("Unknown platform!")
+}
+
+lazy val javaFXModules = Seq("base", "controls", "fxml", "graphics", "media", "swing", "web")
+libraryDependencies ++= javaFXModules.map( m =>
+ // with this the modules will NOT be included in the fat jar!
+ // "org.openjfx" % s"javafx-$m" % javaVersion % "provided" classifier osName
+ "org.openjfx" % s"javafx-$m" % javaVersion.toString classifier osName
+)
+
+assemblyMergeStrategy in assembly := {
+ case PathList("module-info.class", xs @ _*) => MergeStrategy.discard
+ case PathList("META-INF", "MANIFEST.MF", xs @ _*) => MergeStrategy.discard
+ case x => MergeStrategy.first
+}
+
+mainClass in assembly := Some("TLCockpit.ApplicationMain")
+
+assemblyJarName in assembly := "tlcockpit-jdk12.jar"
+assemblyOutputPath in assembly := file("jar/tlcockpit-jdk12.jar")
+
+
+// for scalafx
+fork := true
diff --git a/support/tlcockpit/build.sbt-jdk8 b/support/tlcockpit/build.sbt-jdk8
new file mode 100644
index 0000000000..ac19d09077
--- /dev/null
+++ b/support/tlcockpit/build.sbt-jdk8
@@ -0,0 +1,30 @@
+name := "tlcockpit"
+
+version := "1.1"
+
+scalaVersion := "2.12.7"
+
+val javaVersion:Int = 8
+
+libraryDependencies += "org.scalafx" %% "scalafx" % "8.0.181-R13"
+libraryDependencies += "io.spray" %% "spray-json" % "1.3.3"
+libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2"
+libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
+libraryDependencies += "org.json4s" %% "json4s-native" % "3.6.0-M3"
+libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.6.0-M3"
+
+lazy val root = (project in file(".")).
+ enablePlugins(BuildInfoPlugin).
+ settings(
+ buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion, BuildInfoKey(("javaVersion", javaVersion))),
+ buildInfoPackage := "TLCockpit"
+ )
+
+mainClass in assembly := Some("TLCockpit.ApplicationMain")
+
+assemblyJarName in assembly := "tlcockpit-jdk8.jar"
+assemblyOutputPath in assembly := file("jar/tlcockpit-jdk8.jar")
+
+
+// for scalafx
+fork := true
diff --git a/support/tlcockpit/logo/tlcockpit-banner.jpg b/support/tlcockpit/logo/tlcockpit-banner.jpg
new file mode 100644
index 0000000000..8985309375
--- /dev/null
+++ b/support/tlcockpit/logo/tlcockpit-banner.jpg
Binary files differ
diff --git a/support/tlcockpit/logo/tlcockpit-banner.xcf b/support/tlcockpit/logo/tlcockpit-banner.xcf
new file mode 100644
index 0000000000..b4440043e7
--- /dev/null
+++ b/support/tlcockpit/logo/tlcockpit-banner.xcf
Binary files differ
diff --git a/support/tlcockpit/logo/tlcockpit.xcf b/support/tlcockpit/logo/tlcockpit.xcf
new file mode 100644
index 0000000000..ef7afaae26
--- /dev/null
+++ b/support/tlcockpit/logo/tlcockpit.xcf
Binary files differ
diff --git a/support/tlcockpit/project/assembly.sbt b/support/tlcockpit/project/assembly.sbt
new file mode 100644
index 0000000000..d6540b84c5
--- /dev/null
+++ b/support/tlcockpit/project/assembly.sbt
@@ -0,0 +1 @@
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.8")
diff --git a/support/tlcockpit/project/build.properties b/support/tlcockpit/project/build.properties
new file mode 100644
index 0000000000..091249b92e
--- /dev/null
+++ b/support/tlcockpit/project/build.properties
@@ -0,0 +1 @@
+sbt.version = 1.2.6
diff --git a/support/tlcockpit/project/plugins.sbt b/support/tlcockpit/project/plugins.sbt
new file mode 100644
index 0000000000..15fe71aab1
--- /dev/null
+++ b/support/tlcockpit/project/plugins.sbt
@@ -0,0 +1 @@
+addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0")
diff --git a/support/tlcockpit/screenshots/tlcockpit-backups.jpg b/support/tlcockpit/screenshots/tlcockpit-backups.jpg
new file mode 100644
index 0000000000..b63c62214f
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-backups.jpg
Binary files differ
diff --git a/support/tlcockpit/screenshots/tlcockpit-collections.jpg b/support/tlcockpit/screenshots/tlcockpit-collections.jpg
new file mode 100644
index 0000000000..d21be7098b
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-collections.jpg
Binary files differ
diff --git a/support/tlcockpit/screenshots/tlcockpit-config.jpg b/support/tlcockpit/screenshots/tlcockpit-config.jpg
new file mode 100644
index 0000000000..efd6b1bf90
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-config.jpg
Binary files differ
diff --git a/support/tlcockpit/screenshots/tlcockpit-loading.jpg b/support/tlcockpit/screenshots/tlcockpit-loading.jpg
new file mode 100644
index 0000000000..9296cc582e
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-loading.jpg
Binary files differ
diff --git a/support/tlcockpit/screenshots/tlcockpit-menu-debug-expert.jpg b/support/tlcockpit/screenshots/tlcockpit-menu-debug-expert.jpg
new file mode 100644
index 0000000000..0ec2ac43c7
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-menu-debug-expert.jpg
Binary files differ
diff --git a/support/tlcockpit/screenshots/tlcockpit-packages.jpg b/support/tlcockpit/screenshots/tlcockpit-packages.jpg
new file mode 100644
index 0000000000..5feb050a96
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-packages.jpg
Binary files differ
diff --git a/support/tlcockpit/screenshots/tlcockpit-paper.jpg b/support/tlcockpit/screenshots/tlcockpit-paper.jpg
new file mode 100644
index 0000000000..354c3d3560
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-paper.jpg
Binary files differ
diff --git a/support/tlcockpit/screenshots/tlcockpit-pkgdetails.jpg b/support/tlcockpit/screenshots/tlcockpit-pkgdetails.jpg
new file mode 100644
index 0000000000..01aa4b93dd
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-pkgdetails.jpg
Binary files differ
diff --git a/support/tlcockpit/screenshots/tlcockpit-postactions.jpg b/support/tlcockpit/screenshots/tlcockpit-postactions.jpg
new file mode 100644
index 0000000000..4097948f9e
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-postactions.jpg
Binary files differ
diff --git a/support/tlcockpit/screenshots/tlcockpit-repositories.jpg b/support/tlcockpit/screenshots/tlcockpit-repositories.jpg
new file mode 100644
index 0000000000..ae96799922
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-repositories.jpg
Binary files differ
diff --git a/support/tlcockpit/screenshots/tlcockpit-updates.jpg b/support/tlcockpit/screenshots/tlcockpit-updates.jpg
new file mode 100644
index 0000000000..9e7a8ef24f
--- /dev/null
+++ b/support/tlcockpit/screenshots/tlcockpit-updates.jpg
Binary files differ
diff --git a/support/tlcockpit/scripts/tlcockpit.sh b/support/tlcockpit/scripts/tlcockpit.sh
new file mode 100755
index 0000000000..1b2e8d9cb0
--- /dev/null
+++ b/support/tlcockpit/scripts/tlcockpit.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+# Public domain. Originally written by Norbert Preining and Karl Berry, 2018-2019.
+
+scriptname=`basename "$0"`
+javaVersion=$(java -version 2>&1 | head -1)
+case "$javaVersion" in
+ *"1.8"*) jar="$scriptname-jdk8".jar ;;
+ *) jar="$scriptname.jar" ;;
+esac
+jarpath=`kpsewhich --progname="$scriptname" --format=texmfscripts "$jar"`
+
+kernel=`uname -s 2>/dev/null`
+if echo "$kernel" | grep CYGWIN >/dev/null; then
+ CYGWIN_ROOT=`cygpath -w /`
+ export CYGWIN_ROOT
+ jarpath=`cygpath -w "$jarpath"`
+fi
+
+exec java -jar "$jarpath" "$@"
+
diff --git a/support/tlcockpit/src/main/resources/TLCockpit/spinner-small.gif b/support/tlcockpit/src/main/resources/TLCockpit/spinner-small.gif
new file mode 100644
index 0000000000..10dfdccdbd
--- /dev/null
+++ b/support/tlcockpit/src/main/resources/TLCockpit/spinner-small.gif
Binary files differ
diff --git a/support/tlcockpit/src/main/resources/TLCockpit/spinner.gif b/support/tlcockpit/src/main/resources/TLCockpit/spinner.gif
new file mode 100644
index 0000000000..bff38ea7a9
--- /dev/null
+++ b/support/tlcockpit/src/main/resources/TLCockpit/spinner.gif
Binary files differ
diff --git a/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-128.jpg b/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-128.jpg
new file mode 100644
index 0000000000..fe92ead1e3
--- /dev/null
+++ b/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-128.jpg
Binary files differ
diff --git a/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-48.jpg b/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-48.jpg
new file mode 100644
index 0000000000..d9d5dc6ca8
--- /dev/null
+++ b/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-48.jpg
Binary files differ
diff --git a/support/tlcockpit/src/main/resources/logback.xml b/support/tlcockpit/src/main/resources/logback.xml
new file mode 100644
index 0000000000..ee899bf75d
--- /dev/null
+++ b/support/tlcockpit/src/main/resources/logback.xml
@@ -0,0 +1,11 @@
+<configuration>
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="info">
+ <appender-ref ref="STDOUT" />
+ </root>
+</configuration> \ No newline at end of file
diff --git a/support/tlcockpit/src/main/scala/TLCockpit/ApplicationMain.scala b/support/tlcockpit/src/main/scala/TLCockpit/ApplicationMain.scala
new file mode 100644
index 0000000000..4574ac4966
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TLCockpit/ApplicationMain.scala
@@ -0,0 +1,1643 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package TLCockpit
+
+import javafx.scene.Node
+
+import TLCockpit.Utils._
+import TeXLive._
+import TeXLive.OsTools._
+
+import scala.collection.{immutable, mutable}
+import scala.collection.mutable.ArrayBuffer
+import scala.concurrent.{Future, Promise, SyncVar}
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.io.Source
+import scala.util.{Failure, Success}
+import scala.sys.process._
+import scalafx.beans.property.BooleanProperty
+import scalafx.scene.text.Font
+// ScalaFX imports
+import scalafx.event.Event
+import scalafx.beans.property.{ObjectProperty, StringProperty}
+import scalafx.geometry.{Pos, Orientation}
+import scalafx.scene.Cursor
+import scalafx.scene.control.Alert.AlertType
+import scalafx.scene.image.{Image, ImageView}
+import scalafx.scene.input.{KeyCode, KeyEvent, MouseEvent}
+import scalafx.scene.paint.Color
+// needed see https://github.com/scalafx/scalafx/issues/137
+import scalafx.scene.control.TableColumn._
+import scalafx.scene.control.TreeTableColumn._
+import scalafx.scene.control.TreeItem
+import scalafx.scene.control.Menu._
+import scalafx.scene.control.ListCell
+import scalafx.Includes._
+import scalafx.application.{JFXApp, Platform}
+import scalafx.application.JFXApp.PrimaryStage
+import scalafx.geometry.Insets
+import scalafx.scene.Scene
+import scalafx.scene.layout._
+import scalafx.scene.control._
+import scalafx.event.ActionEvent
+import scalafx.collections.ObservableBuffer
+import scalafx.collections.ObservableMap
+
+// JSON support - important load TLPackageJsonProtocol later!
+import spray.json._
+import TeXLive.JsonProtocol._
+import org.json4s._
+import org.json4s.jackson.JsonMethods._
+
+
+// logging
+import com.typesafe.scalalogging.LazyLogging
+import ch.qos.logback.classic.{Level,Logger}
+import org.slf4j.LoggerFactory
+
+object ApplicationMain extends JFXApp with LazyLogging {
+
+ val version: String = getClass.getPackage.getImplementationVersion
+
+ // parse command line arguments
+ // nothing => INFO
+ // -q WARN -qq ERROR
+ // -d => DEBUG -dd => TRACE
+ val cmdlnlog: Int = parameters.unnamed.map( {
+ case "-d" => Level.DEBUG_INT
+ case "-dd" => Level.TRACE_INT
+ case "-q" => Level.WARN_INT
+ case "-qq" => Level.ERROR_INT
+ case _ => -1
+ } ).foldLeft(Level.OFF_INT)(scala.math.min)
+ if (cmdlnlog == -1) {
+ // Unknown log level has been passed in, error out
+ Console.err.println("Unsupported command line argument passed in, terminating.")
+ Platform.exit()
+ sys.exit(0)
+ }
+ // if nothing has been passed on the command line, use INFO
+ val newloglevel: Int = if (cmdlnlog == Level.OFF_INT) Level.INFO_INT else cmdlnlog
+
+ LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME).
+ asInstanceOf[Logger].setLevel(Level.toLevel(newloglevel))
+
+ logger.trace("starting program tlcockpit")
+
+ val javaVersion = System.getProperty("java.version")
+ val javaVersionSplit: Array[String] = javaVersion.split('.')
+ logger.debug(s"Got javaVersion ${javaVersion}")
+ if (javaVersionSplit.length == 1) {
+ logger.warn(s"Cannot find Java version from ${javaVersion}, continuing anyway!")
+ } else {
+ val major = toInt(javaVersionSplit(0))
+ val minor = toInt(javaVersionSplit(1))
+ major match {
+ case Some(i) =>
+ if (major.get == 1) {
+ minor match {
+ case Some(j) =>
+ if (minor.get < 8) {
+ logger.error(s"Java version ${javaVersion} too old, need >= 1.8, terminating!")
+ Platform.exit()
+ sys.exit(1)
+ } else if (minor.get == 8) {
+ if (BuildInfo.javaVersion != 8) {
+ logger.warn(s"Build and run versions disagree: build: ${BuildInfo.javaVersion}, run: ${major.get}.${minor.get}, trying anyway!")
+ }
+ }
+ case None =>
+ logger.warn(s"Cannot find Java version from ${javaVersion}, continuing anyway!")
+ }
+ } else {
+ if (major.get > 9) {
+ if (major.get != BuildInfo.javaVersion) {
+ logger.warn(s"Build and run versions disagree: build: ${BuildInfo.javaVersion}, run: ${major.get}, trying anyway!")
+ }
+ } else {
+ logger.warn(s"Strange version number, please report: ${javaVersion}, continuing anyway!")
+ }
+ }
+ case None =>
+ logger.warn(s"Cannot find Java version from ${javaVersion}, continuing anyway!")
+ }
+ // in all other cases just hope it is fine
+ logger.info(s"Running on Java Version ${javaVersion}")
+ }
+
+
+ var tlmgrBusy = BooleanProperty(false)
+
+ // necessary action when Window is closed with X or some other operation
+ override def stopApp(): Unit = {
+ tlmgr.cleanup()
+ }
+
+ val iconImage = new Image(getClass.getResourceAsStream("tlcockpit-48.jpg"))
+ val logoImage = new Image(getClass.getResourceAsStream("tlcockpit-128.jpg"))
+ val busyImage = new Image(getClass.getResourceAsStream("spinner-small.gif"))
+ val msgFont = new Font(30f)
+
+ val busySpinner: ImageView = new ImageView(busyImage) {
+ // scaleX = 0.3
+ // scaleY = 0.3
+ }
+
+ def SpinnerPlaceHolder(txt: String): Node = {
+ val tmp = new Label(txt)
+ tmp.wrapText = true
+ tmp.opacity = 0.4f
+ tmp.font = msgFont
+ tmp.graphic = busySpinner
+ tmp
+ }
+
+ val tlpkgs: ObservableMap[String, TLPackageShort] = ObservableMap[String,TLPackageShort]()
+ val pkgs: ObservableMap[String, TLPackageDisplay] = ObservableMap[String, TLPackageDisplay]()
+ val upds: ObservableMap[String, TLUpdateDisplay] = ObservableMap[String, TLUpdateDisplay]()
+ val bkps: ObservableMap[String, Map[String, TLBackupDisplay]] = ObservableMap[String, Map[String,TLBackupDisplay]]() // pkgname -> (version -> TLBackup)*
+
+ val logText: ObservableBuffer[String] = ObservableBuffer[String]()
+ val outputText: ObservableBuffer[String] = ObservableBuffer[String]()
+ val errorText: ObservableBuffer[String] = ObservableBuffer[String]()
+
+ val outputfield: TextArea = new TextArea {
+ editable = false
+ wrapText = true
+ text = ""
+ }
+ val logfield: TextArea = new TextArea {
+ editable = false
+ wrapText = true
+ text = ""
+ }
+ val errorfield: TextArea = new TextArea {
+ editable = false
+ wrapText = true
+ text = ""
+ }
+ logText.onChange({
+ logfield.text = logText.mkString("\n")
+ logfield.scrollTop = Double.MaxValue
+ })
+ errorText.onChange({
+ errorfield.text = errorText.mkString("\n")
+ errorfield.scrollTop = Double.MaxValue
+ if (errorfield.text.value.nonEmpty) {
+ outerrpane.expanded = true
+ outerrtabs.selectionModel().select(2)
+ }
+ })
+ outputText.onChange({
+ outputfield.text = outputText.mkString("\n")
+ outputfield.scrollTop = Double.MaxValue
+ })
+
+ val update_all_menu: MenuItem = new MenuItem("Update all") {
+ val cmd: String = "--all" + {
+ if (disable_auto_install) " --no-auto-install" else "" } + {
+ if (disable_auto_removal) " --no-auto-remove" else "" } + {
+ if (enable_reinstall_forcible) " --reinstall-forcibly-removed" else "" }
+ onAction = (ae) => callback_update(cmd)
+ disable = true
+ }
+ val update_self_menu: MenuItem = new MenuItem("Update self") {
+ onAction = (ae) => callback_update("--self")
+ disable = true
+ }
+
+ val outerrtabs: TabPane = new TabPane {
+ minWidth = 400
+ tabs = Seq(
+ new Tab {
+ text = "Output"
+ closable = false
+ content = outputfield
+ },
+ new Tab {
+ text = "Logging"
+ closable = false
+ content = logfield
+ },
+ new Tab {
+ text = "Errors"
+ closable = false
+ content = errorfield
+ }
+ )
+ }
+ val outerrpane: TitledPane = new TitledPane {
+ text = "Debug"
+ collapsible = true
+ expanded = false
+ content = outerrtabs
+ }
+
+ val cmdline = new TextField()
+ cmdline.onKeyPressed = {
+ (ae: KeyEvent) => if (ae.code == KeyCode.Enter) callback_run_cmdline()
+ }
+
+
+
+ // read the perl dump of ctan mirrors by converting it to JSON code and parsing it
+ def parse_ctan_mirrors(tlroot: String): Map[String,Map[String,Seq[String]]] = {
+ try {
+ val fileName = tlroot + "/tlpkg/installer/ctan-mirrors.pl"
+ val foo: String = Source.fromFile(fileName).getLines.mkString("")
+ val jsonMirrorString = foo.substring(10).replace("=>", ":").replace("""'""", "\"").replace(";", "")
+ val ast = jsonMirrorString.parseJson
+ ast.convertTo[Map[String, Map[String, Map[String, Int]]]].map {
+ contpair =>
+ (contpair._1, contpair._2.map {
+ countrypair => (countrypair._1, countrypair._2.keys.toSeq)
+ })
+ }
+ } catch { case e: Exception =>
+ logText.append("Cannot find or parse ctan-mirrors.pl")
+ logger.debug("Cannot find or parse ctan-mirrors.pl")
+ Map[String,Map[String,Seq[String]]]()
+ }
+ }
+
+ def callback_quit(): Unit = {
+ tlmgr.cleanup()
+ Platform.exit()
+ sys.exit(0)
+ }
+
+ def callback_run_text(s: String): Unit = {
+ tlmgr_send(s, (a: String, b: Array[String]) => {})
+ }
+
+ def callback_run_cmdline(): Unit = {
+ tlmgr_send(cmdline.text.value, (status,output) => {
+ outputText.append(output.mkString("\n"))
+ outerrpane.expanded = true
+ outerrtabs.selectionModel().select(0)
+ })
+ }
+
+ def not_implemented_info(): Unit = {
+ new Alert(AlertType.Warning) {
+ initOwner(stage)
+ title = "Warning"
+ headerText = "This functionality is not implemented by now!"
+ contentText = "Sorry for the inconveniences."
+ }.showAndWait()
+ }
+
+ val OutputBuffer: ObservableBuffer[String] = ObservableBuffer[String]()
+ var OutputBufferIndex:Int = 0
+ val OutputFlushLines = 100
+ OutputBuffer.onChange {
+ // length is number of lines!
+ var foo = ""
+ OutputBuffer.synchronized(
+ if (OutputBuffer.length - OutputBufferIndex > OutputFlushLines) {
+ foo = OutputBuffer.slice(OutputBufferIndex, OutputBufferIndex + OutputFlushLines).mkString("")
+ OutputBufferIndex += OutputFlushLines
+ Platform.runLater {
+ outputText.append(foo)
+ }
+ }
+ )
+ }
+ def reset_output_buffer(): Unit = {
+ OutputBuffer.clear()
+ OutputBufferIndex = 0
+ }
+
+ def callback_run_external(ss: Array[String], unbuffered: Boolean = true): Unit = {
+ outputText.clear()
+ // logText.clear()
+ outerrpane.expanded = true
+ outerrtabs.selectionModel().select(0)
+ // outputText.append(s"Running ${ss.mkString(" ")}" + (if (unbuffered) " (unbuffered)" else " (buffered)"))
+ val foo = Future {
+ ss.foreach { s =>
+ val runcmd = if (isCygwin) "bash -l -c \"" + s + "\"" else s
+ Platform.runLater {
+ outputText.append(s"Running ${s}" + (if (unbuffered) " (unbuffered)" else " (buffered)"))
+ actionLabel.text = s"[${s}]"
+ }
+ runcmd ! ProcessLogger(
+ line => if (unbuffered) Platform.runLater(outputText.append(line))
+ else OutputBuffer.synchronized(OutputBuffer.append(line + "\n")),
+ line => Platform.runLater(logText.append(line))
+ )
+ }
+ }
+ foo.onComplete {
+ case Success(ret) =>
+ Platform.runLater {
+ actionLabel.text = ""
+ outputText.append(OutputBuffer.slice(OutputBufferIndex,OutputBuffer.length).mkString(""))
+ outputText.append("Completed")
+ reset_output_buffer()
+ outputfield.scrollTop = Double.MaxValue
+ }
+ case Failure(t) =>
+ Platform.runLater {
+ actionLabel.text = ""
+ outputText.append(OutputBuffer.slice(OutputBufferIndex,OutputBuffer.length).mkString(""))
+ outputText.append("Completed")
+ reset_output_buffer()
+ outputfield.scrollTop = Double.MaxValue
+ errorText.append(s"An ERROR has occurred running one of ${ss.mkString(" ")}: " + t.getMessage)
+ errorfield.scrollTop = Double.MaxValue
+ outerrpane.expanded = true
+ outerrtabs.selectionModel().select(2)
+ }
+ }
+ }
+
+ def callback_about(): Unit = {
+ new Alert(AlertType.Information) {
+ initOwner(stage)
+ title = "About TLCockpit"
+ graphic = new ImageView(logoImage)
+ headerText = "TLCockpit version " + version + "\n\nManage your TeX Live with speed!"
+ contentText = "Copyright 2017-2018 Norbert Preining\nLicense: GPL3+\nSources: https://github.com/TeX-Live/tlcockpit"
+ }.showAndWait()
+ }
+
+ // Output of update --self
+ /*
+tlmgr>
+update --self
+location-url /home/norbert/public_html/tlnet /home/norbert/public_html/tlcritical /home/norbert/Domains/server/texlive.info/contrib/2017 /home/norbert/public_html/tltexjp
+total-bytes 381087
+end-of-header
+texlive.infra u 4629 46295 381087 ??:?? ??:?? tlcritical - -
+end-of-updates
+STDERR running mktexlsr ...
+STDERR done running mktexlsr.
+STDERR running mtxrun --generate ...
+STDERR done running mtxrun --generate.
+OK
+STDOUT (with patch STDERR) tlmgr has been updated, restarting!
+protocol 1
+tlmgr>
+
+ The problem with the update function lies in the
+ protocol 1
+ which is not accepted/expected by the update function!
+ */
+ def set_line_update_function(mode: String): Unit = {
+ var prevName = ""
+ stdoutLineUpdateFunc = (l:String) => {
+ logger.trace("DEBUG line update: " + l + "=")
+ l match {
+ case u if u.startsWith("location-url") => None
+ case u if u.startsWith("total-bytes") => None
+ case u if u.startsWith("end-of-header") => None
+ // case u if u.startsWith("end-of-updates") => None
+ case u if u == "OK" => None
+ case u if u.startsWith("tlmgr>") => None
+ case u =>
+ if (prevName != "") {
+ if (mode == "update") {
+ // parallelism is a pain, I get concurrent access here, but don't know with whom?
+ // ConcurrentModificationExceptions often occur when you're modifying
+ // a collection while you are iterating over its elements.
+ // val newkids: ObservableBuffer[TreeItem[TLUpdateDisplay]] =
+ // updateTable.root.value.children.filter(_.value.value.name.value != prevName)
+ // .map(_.asInstanceOf[TreeItem[TLUpdateDisplay]])
+ // the last map is only necessary becasue ScalaFX is buggy and does not produce
+ // proper types here!!! Probably similar to https://github.com/scalafx/scalafx/issues/137
+ // updateTable.root.value.children = newkids
+ upds.remove(prevName)
+ trigger_update("upds")
+ } else if (mode == "remove") {
+ tlpkgs(prevName).installed = false
+ } else { // install
+ tlpkgs(prevName).installed = true
+ }
+ if (mode == "remove") {
+ pkgs(prevName).lrev = ObjectProperty[Int](0)
+ pkgs(prevName).installed = StringProperty("Not installed")
+ tlpkgs(prevName).lrev = 0
+ } else { // install and update
+ pkgs(prevName).lrev = pkgs(prevName).rrev
+ pkgs(prevName).installed = StringProperty("Installed") // TODO support Mixed!!!
+ tlpkgs(prevName).lrev = tlpkgs(prevName).rrev
+ }
+ packageTable.refresh()
+ }
+ if (u.startsWith("end-of-updates")) {
+ if (mode == "update") {
+ Platform.runLater {
+ updateTable.placeholder = SpinnerPlaceHolder("Post actions running")
+ actionLabel.text = "[post actions running]"
+ }
+ }
+ // nothing to be done, all has been done above
+ logger.debug("DEBUG got end of updates")
+ // } else if (u.startsWith("protocol ")) {
+ // logger.debug("Got protocol line, seems tlmgr got updated and restarted!")
+ // // nothing else to be done
+ // } else if (u.startsWith("tlmgr has been updated, restarting")) {
+ // logger.debug("tlmgr got updated and restarted, ignoring output")
+ } else {
+ logger.debug("DEBUG getting update line")
+ prevName = if (mode == "update") {
+ val foo = parse_one_update_line(l)
+ val pkgname = foo.name.value
+ upds(pkgname).status = StringProperty("Updating ...")
+ updateTable.refresh()
+ pkgname
+ } else if (mode == "install") {
+ val fields = l.split("\t")
+ val pkgname = fields(0)
+ pkgs(pkgname).installed = StringProperty("Installing ...")
+ packageTable.refresh()
+ pkgname
+ } else { // remove
+ val fields = l.split("\t")
+ val pkgname = fields(0)
+ pkgs(pkgname).installed = StringProperty("Removing ...")
+ packageTable.refresh()
+ pkgname
+ }
+ }
+ }
+ }
+ }
+
+ def callback_update(s: String): Unit = {
+ val prevph = updateTable.placeholder.value
+ set_line_update_function("update")
+ val cmd = if (s == "--self") "update --self --no-restart" else s"update $s"
+ tlmgr_send(cmd, (a,b) => {
+ stdoutLineUpdateFunc = defaultStdoutLineUpdateFunc
+ Platform.runLater {
+ updateTable.placeholder = prevph
+ }
+ if (s == "--self") {
+ reinitialize_tlmgr()
+ // this doesn't work seemingly
+ // update_upds_list()
+ }
+ })
+ }
+
+ def callback_remove(pkg: String): Unit = {
+ set_line_update_function("remove")
+ tlmgr_send(s"remove $pkg", (_, _) => {
+ stdoutLineUpdateFunc = defaultStdoutLineUpdateFunc
+ })
+ }
+ def callback_install(pkg: String): Unit = {
+ set_line_update_function("install")
+ tlmgr_send(s"install $pkg", (_,_) => {
+ stdoutLineUpdateFunc = defaultStdoutLineUpdateFunc
+ })
+ }
+
+
+ def callback_restore(str: String, rev: String): Unit = {
+ tlmgr_send(s"restore --force $str $rev", (_,_) => {
+ tlpkgs(str).lrev = rev.toLong
+ pkgs(str).lrev = ObjectProperty[Int](rev.toInt)
+ packageTable.refresh()
+ Platform.runLater { actionLabel.text = "[running post actions]" }
+ })
+ }
+
+ bkps.onChange( (obs,chs) => {
+ val doit = chs match {
+ case ObservableMap.Add(k, v) => k.toString == "root"
+ case ObservableMap.Replace(k, va, vr) => k.toString == "root"
+ case ObservableMap.Remove(k, v) => k.toString == "root"
+ }
+ if (doit) {
+ logger.debug("DEBUG bkps.onChange called new length = " + bkps.keys.toArray.length)
+ val newroot = new TreeItem[TLBackupDisplay](new TLBackupDisplay("root", "", "")) {
+ children = bkps
+ .filter(_._1 != "root")
+ .map(p => {
+ val pkgname: String = p._1
+ // we sort by negative of revision number, which give inverse sort
+ val versmap: Array[(String, TLBackupDisplay)] = p._2.toArray.sortBy(-_._2.rev.value.toInt)
+
+ val foo: Seq[TreeItem[TLBackupDisplay]] = versmap.tail.sortBy(-_._2.rev.value.toInt).map { q =>
+ new TreeItem[TLBackupDisplay](q._2)
+ }.toSeq
+ new TreeItem[TLBackupDisplay](versmap.head._2) {
+ children = foo
+ }
+ }).toArray.sortBy(_.value.value.name.value)
+ }
+ Platform.runLater {
+ backupTable.root = newroot
+ }
+ }
+ })
+
+ def view_pkgs_by_collections(pkgbuf: scala.collection.mutable.Map[String, TLPackageDisplay],
+ binbuf: scala.collection.mutable.Map[String, ArrayBuffer[TLPackageDisplay]],
+ colbuf: scala.collection.mutable.Map[String, ArrayBuffer[TLPackageDisplay]]): Seq[TreeItem[TLPackageDisplay]] = {
+ val bin_pkg_map = compute_bin_pkg_mapping(pkgbuf, binbuf)
+ colbuf.map(
+ p => {
+ val colname: String = p._1
+ val coldeps: Seq[TLPackageDisplay] = p._2
+ val coltlpd: TLPackageDisplay = pkgbuf(colname)
+
+ new TreeItem[TLPackageDisplay](coltlpd) {
+ children = coldeps.filter(q => tlpkgs(q.name.value).category != "Collection").sortBy(_.name.value).map(sub => {
+ val binmap: (Boolean, Seq[TLPackageDisplay]) = bin_pkg_map(sub.name.value)
+ val ismixed: Boolean = binmap._1
+ val kids: Seq[TLPackageDisplay] = binmap._2.sortBy(_.name.value)
+ val ti = if (ismixed) {
+ // replace installed status with "Mixed"
+ new TreeItem[TLPackageDisplay](
+ new TLPackageDisplay(sub.name.value, sub.lrev.value.toString, sub.rrev.value.toString, sub.shortdesc.value, sub.size.value.toString, "Mixed")
+ ) {
+ children = kids.map(new TreeItem[TLPackageDisplay](_))
+ }
+ } else {
+ new TreeItem[TLPackageDisplay](sub) {
+ children = kids.map(new TreeItem[TLPackageDisplay](_))
+ }
+ }
+ ti
+ }
+ )
+ }
+ }
+ ).toSeq
+ // ArrayBuffer.empty[TreeItem[TLPackageDisplay]]
+ }
+
+ def view_pkgs_by_names(pkgbuf: scala.collection.mutable.Map[String, TLPackageDisplay],
+ binbuf: scala.collection.mutable.Map[String, ArrayBuffer[TLPackageDisplay]]): Seq[TreeItem[TLPackageDisplay]] = {
+ val bin_pkg_map: Map[String, (Boolean, Seq[TLPackageDisplay])] = compute_bin_pkg_mapping(pkgbuf, binbuf)
+ pkgbuf.map{
+ p => {
+ val binmap: (Boolean, Seq[TLPackageDisplay]) = bin_pkg_map(p._1)
+ val pkgtlp: TLPackageDisplay = p._2
+ val ismixed: Boolean = binmap._1
+ val kids: Seq[TLPackageDisplay] = binmap._2.sortBy(_.name.value)
+ if (ismixed) {
+ new TreeItem[TLPackageDisplay](
+ new TLPackageDisplay(pkgtlp.name.value, pkgtlp.lrev.value.toString, pkgtlp.rrev.value.toString, pkgtlp.shortdesc.value, pkgtlp.size.value.toString, "Mixed")
+ ) {
+ children = kids.map(new TreeItem[TLPackageDisplay](_))
+ }
+ } else {
+ new TreeItem[TLPackageDisplay](pkgtlp) {
+ children = kids.map(new TreeItem[TLPackageDisplay](_))
+ }
+ }
+ }
+ }.toSeq
+ }
+
+ def compute_bin_pkg_mapping(pkgbuf: scala.collection.mutable.Map[String, TLPackageDisplay],
+ binbuf: scala.collection.mutable.Map[String, ArrayBuffer[TLPackageDisplay]]): Map[String, (Boolean, Seq[TLPackageDisplay])] = {
+ pkgbuf.map {
+ p => {
+ val kids: Seq[TLPackageDisplay] = if (binbuf.keySet.contains(p._2.name.value)) {
+ binbuf(p._2.name.value)
+ } else {
+ Seq()
+ }
+ // for ismixed we && all the installed status. If all are installed, we get true
+ val allinstalled = (kids :+ p._2).foldRight[Boolean](true)((k, b) => k.installed.value == "Installed" && b)
+ val someinstalled = (kids :+ p._2).exists(_.installed.value == "Installed")
+ val mixedinstalled = !allinstalled && someinstalled
+ (p._1, (mixedinstalled, kids))
+ }
+ }.toMap
+ }
+ pkgs.onChange( (obs,chs) => {
+ val doit = chs match {
+ case ObservableMap.Add(k, v) => k.toString == "root"
+ case ObservableMap.Replace(k, va, vr) => k.toString == "root"
+ // don't call the trigger on root removal!
+ // case ObservableMap.Remove(k, v) => k.toString == "root"
+ case ObservableMap.Remove(k,v) => false
+ }
+ if (doit) {
+ logger.debug("DEBUG: entering pkgs.onChange")
+ // val pkgbuf: ArrayBuffer[TLPackageDisplay] = ArrayBuffer.empty[TLPackageDisplay]
+ val pkgbuf = scala.collection.mutable.Map.empty[String, TLPackageDisplay]
+ val binbuf = scala.collection.mutable.Map.empty[String, ArrayBuffer[TLPackageDisplay]]
+ val colbuf = scala.collection.mutable.Map.empty[String, ArrayBuffer[TLPackageDisplay]]
+ pkgs.foreach(pkg => {
+ // complicated part, determine whether it is a sub package or not!
+ // we strip of initial texlive. prefixes to make sure we deal
+ // with real packages
+ if ((pkg._1.startsWith("texlive.infra") && pkg._1.stripPrefix("texlive.infra").contains(".")) ||
+ pkg._1.stripPrefix("texlive.infra").contains(".")) {
+ val foo: Array[String] = if (pkg._1.startsWith("texlive.infra"))
+ Array("texlive.infra", pkg._1.stripPrefix("texlive.infra"))
+ else
+ pkg._1.split('.')
+ val pkgname = foo(0)
+ if (pkgname != "") {
+ val binname = foo(1)
+ if (binbuf.keySet.contains(pkgname)) {
+ binbuf(pkgname) += pkg._2
+ } else {
+ binbuf(pkgname) = ArrayBuffer[TLPackageDisplay](pkg._2)
+ }
+ }
+ } else if (pkg._1 == "root") {
+ // ignore the dummy root element,
+ // only used for speeding up event handling
+ } else {
+ pkgbuf(pkg._1) = pkg._2
+ }
+ })
+ // Another round to propagate purely .win32 packages like wintools.win32 or
+ // dviout.win32 from binpkg status to full pkg, since they don't have
+ // accompanying main packages
+ binbuf.foreach(p => {
+ if (!pkgbuf.contains(p._1)) {
+ if (p._2.length > 1) {
+ errorText += "THAT SHOULD NOT HAPPEN: >>" + p._1 + "<< >>" + p._2.length + "<<"
+ p._2.foreach(f => logger.trace("-> " + f.name.value))
+ } else {
+ logger.trace("DEBUG Moving " + p._2.head.name.value + " up to pkgbuf " + p._1)
+ pkgbuf(p._2.head.name.value) = p._2.head
+ // TODO will this work out with the foreach loop above???
+ binbuf -= p._1
+ }
+ }
+ })
+ // another loop to collection and fill the collections buffer
+ pkgs.foreach(pkg => {
+ if (tlpkgs.contains(pkg._1)) {
+ if (tlpkgs(pkg._1).category == "Collection") {
+ val foo: immutable.Seq[String] = tlpkgs(pkg._1).depends
+ colbuf(pkg._1) = ArrayBuffer[TLPackageDisplay]()
+ // TODO we need to deal with packages that get removed!!!
+ // for now just make sure we don't add them here!
+ colbuf(pkg._1) ++= foo.filter(pkgbuf.contains).map(pkgbuf(_))
+ }
+ } else if (pkg._1 == "root") {
+ // do nothing
+ } else {
+ errorText += "Cannot find information for " + pkg._1
+ }
+ })
+ // now we have all normal packages in pkgbuf, and its sub-packages in binbuf
+ // we need to create TreeItems
+ val viewkids: Seq[TreeItem[TLPackageDisplay]] =
+ if (ViewByPkg.selected.value)
+ view_pkgs_by_names(pkgbuf, binbuf)
+ else
+ view_pkgs_by_collections(pkgbuf, binbuf, colbuf)
+ logger.debug("DEBUG: leaving pkgs.onChange before runLater")
+ Platform.runLater {
+ packageTable.root = new TreeItem[TLPackageDisplay](new TLPackageDisplay("root", "0", "0", "", "0", "")) {
+ expanded = true
+ children = viewkids.sortBy(_.value.value.name.value)
+ }
+ }
+ }
+ })
+ upds.onChange( (obs, chs) => {
+ var doit = chs match {
+ case ObservableMap.Add(k, v) => k.toString == "root"
+ case ObservableMap.Replace(k, va, vr) => k.toString == "root"
+ case ObservableMap.Remove(k, v) => k.toString == "root"
+ }
+ if (doit) {
+ val infraAvailable = upds.keys.exists(_.startsWith("texlive.infra"))
+ // only allow for updates of other packages when no infra update available
+ val updatesAvailable = !infraAvailable && upds.keys.exists(p => !p.startsWith("texlive.infra") && !(p == "root"))
+ val newroot = new TreeItem[TLUpdateDisplay](new TLUpdateDisplay("root", "", "", "", "", "")) {
+ children = upds
+ .filter(_._1 != "root")
+ .map(p => new TreeItem[TLUpdateDisplay](p._2))
+ .toArray
+ .sortBy(_.value.value.name.value)
+ }
+ Platform.runLater {
+ update_self_menu.disable = !infraAvailable
+ update_all_menu.disable = !updatesAvailable
+ updateTable.root = newroot
+ if (infraAvailable) {
+ texlive_infra_update_warning()
+ }
+ }
+ }
+ })
+
+ def texlive_infra_update_warning(): Unit = {
+ new Alert(AlertType.Warning) {
+ initOwner(stage)
+ title = "TeX Live Infrastructure Update Available"
+ headerText = "Updates to the TeX Live Manager (Infrastructure) available."
+ contentText = "Please use \"Update self\" from the Menu!"
+ }.showAndWait()
+ }
+
+ def load_backups_update_bkps_view(): Unit = {
+ val prevph = backupTable.placeholder.value
+ backupTable.placeholder = SpinnerPlaceHolder("Loading backups")
+ tlmgr_send("restore --json", (status, lines) => {
+ val jsonAst = lines.mkString("").parseJson
+ val backups: Map[String, Map[String, TLBackupDisplay]] =
+ jsonAst
+ .convertTo[List[TLBackup]]
+ .groupBy[String](_.name)
+ .map(p => (p._1, p._2.map(q => (q.rev, new TLBackupDisplay(q.name, q.rev, q.date))).toMap))
+ bkps.clear()
+ bkps ++= backups
+ trigger_update("bkps")
+ backupTable.placeholder = prevph
+ })
+ }
+
+ def update_pkgs_view(): Unit = {
+ val newpkgs: Map[String, TLPackageDisplay] =
+ tlpkgs
+ .filter { p =>
+ val searchTerm = searchEntry.text.value.toLowerCase
+ p._1.toLowerCase.contains(searchTerm) ||
+ p._2.shortdesc.getOrElse("").toLowerCase.contains(searchTerm)
+ }
+ .map { p =>
+ (p._2.name,
+ new TLPackageDisplay(
+ p._2.name, p._2.lrev.toString, p._2.rrev.toString,
+ p._2.shortdesc.getOrElse(""), "0", if (p._2.installed) "Installed" else "Not installed"
+ )
+ )
+ }.toMap
+ pkgs.clear()
+ pkgs ++= newpkgs
+ trigger_update("pkgs")
+ }
+
+ /*
+ def load_tlpdb_update_pkgs_view():Unit = {
+ val prevph = packageTable.placeholder.value
+ packageTable.placeholder = SpinnerPlaceHolder("Loading database")
+ tlmgr_send("info --json", (status, lines) => {
+ logger.debug(s"load tlpdb update pkgs: got status ${status}")
+ logger.trace(s"load tlpdb update pkgs: got lines = " + lines.head)
+ val jsonAst = lines.mkString("").parseJson
+ tlpkgs.clear()
+ tlpkgs ++= jsonAst.convertTo[List[TLPackage]].map { p => (p.name, p)}
+ update_pkgs_view()
+ packageTable.placeholder = prevph
+ })
+ }
+ */
+
+ def load_tlpdb_update_pkgs_view_no_json():Unit = {
+ val prevph = packageTable.placeholder.value
+ packageTable.placeholder = SpinnerPlaceHolder("Loading database")
+ tlmgr_send("info --data name,localrev,remoterev,category,size,installed,depends,shortdesc", (status, lines) => {
+ logger.debug(s"load tlpdb update (no json) pkgs: got status ${status}")
+ logger.trace(s"load tlpdb update (no json) pkgs: got lines = " + lines.head)
+ val newtlpkgs: Map[String, TLPackageShort] = lines.map(l => {
+ // logger.debug(s"got line >>>${l}<<<")
+ val fields = l.split(",",8)
+ val pkgname = fields(0)
+ val shortdesc = fields(7).stripPrefix(""""""").stripSuffix(""""""").replaceAll("""\\"""",""""""")
+ val lrev = fields(1).toLong
+ val rrev = fields(2).toLong
+ val cat = fields(3)
+ val size = fields(4).toLong
+ val installed = fields(5) == "1"
+ val depends = fields(6).split(":").toList
+ TLPackageShort(pkgname, if (shortdesc == "") None else Some(shortdesc), lrev, rrev, cat, depends, installed, lrev > 0)
+ }).map{ p =>
+ logger.trace("Constructed TLPackage: " + p)
+ (p.name, p)
+ }.toMap
+ tlpkgs.clear()
+ tlpkgs ++= newtlpkgs
+ update_pkgs_view()
+ packageTable.placeholder = prevph
+ })
+ }
+
+ def parse_one_update_line(l: String): TLUpdateDisplay = {
+ logger.debug(s"Got update line >>${l}")
+ val fields = l.split("\t")
+ logger.debug(s"Splitting into ${fields}")
+ val pkgname = fields(0)
+ val status = fields(1) match {
+ case "d" => "Removed on server"
+ case "f" => "Forcibly removed"
+ case "u" => "Update available"
+ case "r" => "Local is newer"
+ case "a" => "New on server"
+ case "i" => "Not installed"
+ case "I" => "Reinstall"
+ }
+ val localrev = fields(2)
+ val serverrev = fields(3)
+ val size = if (fields(1) == "d") "0" else humanReadableByteSize(fields(4).toLong)
+ val runtime = fields(5)
+ val esttot = fields(6)
+ val tag = fields(7)
+ val lctanv = fields(8)
+ val rctanv = fields(9)
+ val tlpkg: TLPackageDisplay = pkgs(pkgname)
+ val shortdesc = tlpkg.shortdesc.value
+ new TLUpdateDisplay(pkgname, status,
+ localrev + {
+ if (lctanv != "-") s" ($lctanv)" else ""
+ },
+ serverrev + {
+ if (rctanv != "-") s" ($rctanv)" else ""
+ },
+ shortdesc, size)
+ }
+
+ def load_updates_update_upds_view(): Unit = {
+ val prevph = updateTable.placeholder.value
+ updateTable.placeholder = SpinnerPlaceHolder("Loading updates")
+ tlmgr_send("update --list", (status, lines) => {
+ logger.debug(s"got updates length ${lines.length}")
+ logger.trace(s"tlmgr last output = ${lines}")
+ val newupds: Map[String, TLUpdateDisplay] = lines.filter {
+ case u if u.startsWith("location-url") => false
+ case u if u.startsWith("total-bytes") => false
+ case u if u.startsWith("end-of-header") => false
+ case u if u.startsWith("end-of-updates") => false
+ case u => true
+ }.map { l =>
+ val foo = parse_one_update_line(l)
+ (foo.name.value, foo)
+ }.toMap
+ val infraAvailable = newupds.keys.exists(_.startsWith("texlive.infra"))
+ upds.clear()
+ if (infraAvailable) {
+ upds ++= Seq( ("texlive.infra", newupds("texlive.infra") ) )
+ } else {
+ upds ++= newupds
+ }
+ trigger_update("upds")
+ updateTable.placeholder = prevph
+ })
+ }
+
+ def trigger_update(s:String): Unit = {
+ logger.debug("DEBUG: Triggering update of " + s)
+ if (s == "pkgs") {
+ pkgs("root") = new TLPackageDisplay("root", "0", "0", "", "0", "")
+ } else if (s == "upds") {
+ upds("root") = new TLUpdateDisplay("root", "", "", "", "", "")
+ } else if (s == "bkps") {
+ bkps("root") = Map[String, TLBackupDisplay](("0", new TLBackupDisplay("root", "0", "0")))
+ }
+ }
+
+ def doListView(files: Seq[String], clickable: Boolean): scalafx.scene.Node = {
+ if (files.length <= 5) {
+ val vb = new VBox()
+ vb.children = files.map { f =>
+ val fields = f.split(" ")
+ new Label(fields(0)) {
+ if (clickable) {
+ textFill = Color.Blue
+ onMouseClicked = { me: MouseEvent => OsTools.openFile(tlmgr.tlroot + "/" + fields(0)) }
+ cursor = Cursor.Hand
+ }
+ }
+ }
+ vb
+ } else {
+ val vb = new ListView[String] {}
+ vb.minHeight = 150
+ vb.prefHeight = 150
+ vb.maxHeight = 200
+ vb.vgrow = Priority.Always
+ vb.orientation = Orientation.Vertical
+ vb.cellFactory = { p => {
+ val foo = new ListCell[String]
+ foo.item.onChange { (_, _, str) => foo.text = str }
+ if (clickable) {
+ foo.textFill = Color.Blue
+ foo.onMouseClicked = { me: MouseEvent => OsTools.openFile(tlmgr.tlroot + "/" + foo.text.value) }
+ foo.cursor = Cursor.Hand
+ }
+ foo
+ }
+ }
+ // vb.children = docFiles.map { f =>
+ vb.items = ObservableBuffer(files.map { f =>
+ val fields = f.split(" ")
+ fields(0)
+ })
+ vb
+ }
+ }
+
+ val mainMenu: Menu = new Menu("TLCockpit") {
+ items = List(
+ // temporarily move here as we disable the Help menu
+ new MenuItem("About") {
+ onAction = (ae) => callback_about()
+ },
+ new MenuItem("Exit") {
+ onAction = (ae: ActionEvent) => callback_quit()
+ })
+ }
+ val toolsMenu: Menu = new Menu("Tools") {
+ items = List(
+ new MenuItem("Update filename databases ...") {
+ onAction = (ae) => {
+ callback_run_external(Array("mktexlsr", "mtxrun --generate"))
+ // callback_run_external("mtxrun --generate")
+ }
+ },
+ // too many lines are quickly output -> GUI becomes hanging until
+ // all the callbacks are done - call fmtutil with unbuffered = false
+ new MenuItem("Rebuild all formats ...") { onAction = (ae) => callback_run_external(Array("fmtutil --sys --all"), false) },
+ new MenuItem("Update font map database ...") {
+ onAction = (ae) => callback_run_external(Array("updmap --sys"))
+ }
+ )
+ }
+ val ViewByPkg = new RadioMenuItem("by package name") {
+ onAction = (ae) => {
+ searchEntry.text = ""
+ update_pkgs_view()
+ }
+ }
+ val ViewByCol = new RadioMenuItem("by collections") {
+ onAction = (ae) => {
+ searchEntry.text = ""
+ update_pkgs_view()
+ }
+ }
+ ViewByPkg.selected = true
+ ViewByCol.selected = false
+ val pkgsMenu: Menu = new Menu("Packages") {
+ val foo = new ToggleGroup
+ foo.toggles = Seq(ViewByPkg, ViewByCol)
+ items = List(ViewByPkg, ViewByCol)
+ }
+ var disable_auto_removal = false
+ var disable_auto_install = false
+ var enable_reinstall_forcible = false
+ val updMenu: Menu = new Menu("Updates") {
+ items = List(
+ update_all_menu,
+ update_self_menu,
+ new SeparatorMenuItem,
+ new CheckMenuItem("Disable auto removal") { onAction = (ae) => disable_auto_removal = selected.value },
+ new CheckMenuItem("Disable auto install") { onAction = (ae) => disable_auto_install = selected.value },
+ new CheckMenuItem("Reinstall forcibly removed") { onAction = (ae) => enable_reinstall_forcible = selected.value }
+ )
+ }
+
+
+ def callback_general_options(): Unit = {
+ tlmgr_send("option showall --json", (status, lines) => {
+ val jsonAst = lines.mkString("").parseJson
+ val tlpdopts: List[TLOption] = jsonAst.convertTo[List[TLOption]]
+ Platform.runLater {
+ val dg = new OptionsDialog(tlpdopts)
+ dg.showAndWait() match {
+ case Some(changedOpts) =>
+ changedOpts.foreach(p => {
+ // don't believe it or not, but \" does *NOT* work in Scala in
+ // interpolated strings, and it seems there is no better way
+ // than that one ...
+ tlmgr_send(s"option ${p._1} ${'"'}${p._2}${'"'}", (_,_) => None)
+ })
+ case None =>
+ }
+ }
+ })
+ }
+
+
+ def callback_paper(): Unit = {
+ tlmgr_send("paper --json", (status, lines) => {
+ val jsonAst = lines.mkString("").parseJson
+ val paperconfs: Map[String, TLPaperConf] = jsonAst.convertTo[List[TLPaperConf]].map { p => (p.program, p) }.toMap
+ val currentPapers: Map[String, String] = paperconfs.mapValues(p => p.options.head)
+ Platform.runLater {
+ val dg = new PaperDialog(paperconfs)
+ dg.showAndWait() match {
+ case Some(newPapers) =>
+ logger.debug(s"Got result ${newPapers}")
+ // collect changed settings
+ val changedPapers = newPapers.filter(p => currentPapers(p._1) != p._2)
+ logger.debug(s"Got changed papers ${changedPapers}")
+ changedPapers.foreach(p => {
+ tlmgr_send(s"paper ${p._1} paper ${p._2}", (_,_) => None)
+ })
+ case None =>
+ }
+ }
+ })
+ }
+
+ def callback_pkg_info(pkg: String) = {
+ tlmgr_send(s"info --json ${pkg}", (status, lines) => {
+ try {
+ val jsonAst = lines.mkString("").parseJson
+ val tlpkgs = jsonAst.convertTo[List[TLPackage]]
+ Platform.runLater {
+ new PkgInfoDialog(tlpkgs.head).showAndWait()
+ }
+ } catch {
+ case foo : spray.json.DeserializationException =>
+ new Alert(AlertType.Warning) {
+ initOwner(stage)
+ title = "Warning"
+ headerText = s"Cannot display information for ${pkg}"
+ contentText = s"We couldn't parse the output of\ntlmgr info --json ${pkg}\n"
+ }.showAndWait()
+ case bar : ArrayIndexOutOfBoundsException =>
+ new Alert(AlertType.Warning) {
+ initOwner(stage)
+ title = "Warning"
+ headerText = s"Cannot find information for ${pkg}"
+ contentText = s"We couldn't find information for ${pkg}\n"
+ }.showAndWait()
+ }
+ })
+ }
+
+ val optionsMenu: Menu = new Menu("Options") {
+ items = List(
+ new MenuItem("General ...") { onAction = (ae) => callback_general_options() },
+ new MenuItem("Paper ...") { onAction = (ae) => callback_paper() },
+ /* new MenuItem("Platforms ...") { disable = true; onAction = (ae) => not_implemented_info() },
+ new SeparatorMenuItem,
+ new CheckMenuItem("Expert options") { disable = true },
+ new CheckMenuItem("Enable debugging options") { disable = true },
+ new CheckMenuItem("Disable auto-install of new packages") { disable = true },
+ new CheckMenuItem("Disable auto-removal of server-deleted packages") { disable = true } */
+ )
+ }
+ val expertPane: TitledPane = new TitledPane {
+ text = "Experts only"
+ collapsible = true
+ expanded = false
+ content = new VBox {
+ spacing = 10
+ children = List(
+ new HBox {
+ spacing = 10
+ alignment = Pos.CenterLeft
+ children = List(
+ new Label("tlmgr shell command:"),
+ cmdline,
+ new Button {
+ text = "Go"
+ onAction = (event: ActionEvent) => callback_run_cmdline()
+ }
+ )
+ }
+ )
+ }
+ }
+ val updateTable: TreeTableView[TLUpdateDisplay] = {
+ val colName = new TreeTableColumn[TLUpdateDisplay, String] {
+ text = "Package"
+ cellValueFactory = { _.value.value.value.name }
+ prefWidth = 150
+ }
+ val colStatus = new TreeTableColumn[TLUpdateDisplay, String] {
+ text = "Status"
+ cellValueFactory = { _.value.value.value.status }
+ prefWidth = 120
+ }
+ val colDesc = new TreeTableColumn[TLUpdateDisplay, String] {
+ text = "Description"
+ cellValueFactory = { _.value.value.value.shortdesc }
+ prefWidth = 300
+ }
+ val colLRev = new TreeTableColumn[TLUpdateDisplay, String] {
+ text = "Local rev"
+ cellValueFactory = { _.value.value.value.lrev }
+ prefWidth = 100
+ }
+ val colRRev = new TreeTableColumn[TLUpdateDisplay, String] {
+ text = "Remote rev"
+ cellValueFactory = { _.value.value.value.rrev }
+ prefWidth = 100
+ }
+ val colSize = new TreeTableColumn[TLUpdateDisplay, String] {
+ text = "Size"
+ cellValueFactory = { _.value.value.value.size }
+ prefWidth = 70
+ }
+ val table = new TreeTableView[TLUpdateDisplay](
+ new TreeItem[TLUpdateDisplay](new TLUpdateDisplay("root","","","","","")) {
+ expanded = false
+ }) {
+ columns ++= List(colName, colStatus, colDesc, colLRev, colRRev, colSize)
+ }
+ colDesc.prefWidth.bind(table.width - colName.width - colLRev.width - colRRev.width - colSize.width - colStatus. width - 15)
+ table.prefHeight = 300
+ table.vgrow = Priority.Always
+ table.placeholder = new Label("No updates available") {
+ opacity = 0.4f
+ font = msgFont
+ }
+ table.showRoot = false
+ table.rowFactory = { _ =>
+ val row = new TreeTableRow[TLUpdateDisplay] {}
+ val infoMI = new MenuItem("Info") {
+ onAction = (ae) => callback_pkg_info(row.item.value.name.value)
+ }
+ val updateMI = new MenuItem("Update") {
+ onAction = (ae) => callback_update(row.item.value.name.value)
+ }
+ val installMI = new MenuItem("Install") {
+ onAction = (ae) => callback_install(row.item.value.name.value)
+ }
+ val removeMI = new MenuItem("Remove") {
+ onAction = (ae) => callback_remove(row.item.value.name.value)
+ }
+ val ctm = new ContextMenu(infoMI, updateMI, installMI, removeMI)
+ row.item.onChange { (_,_,newTL) =>
+ if (newTL != null) {
+ if (newTL.status.value == "New on server") {
+ installMI.disable = false
+ removeMI.disable = true
+ updateMI.disable = true
+ } else if (newTL.status.value == "Removed on server") {
+ installMI.disable = true
+ removeMI.disable = false
+ updateMI.disable = true
+ } else {
+ installMI.disable = true
+ removeMI.disable = false
+ updateMI.disable = false
+ }
+ }
+ }
+ row.contextMenu = ctm
+ row
+ }
+ table
+ }
+ val packageTable: TreeTableView[TLPackageDisplay] = {
+ val colName = new TreeTableColumn[TLPackageDisplay, String] {
+ text = "Package"
+ cellValueFactory = { _.value.value.value.name }
+ prefWidth = 150
+ }
+ val colDesc = new TreeTableColumn[TLPackageDisplay, String] {
+ text = "Description"
+ cellValueFactory = { _.value.value.value.shortdesc }
+ prefWidth = 300
+ }
+ val colInst = new TreeTableColumn[TLPackageDisplay, String] {
+ text = "Installed"
+ cellValueFactory = { _.value.value.value.installed }
+ prefWidth = 100
+ }
+ val table = new TreeTableView[TLPackageDisplay](
+ new TreeItem[TLPackageDisplay](new TLPackageDisplay("root","0","0","","0","")) {
+ expanded = false
+ }) {
+ columns ++= List(colName, colDesc, colInst)
+ }
+ colDesc.prefWidth.bind(table.width - colInst.width - colName.width - 15)
+ table.prefHeight = 300
+ table.showRoot = false
+ table.vgrow = Priority.Always
+ table.rowFactory = { p =>
+ val row = new TreeTableRow[TLPackageDisplay] {}
+ val infoMI = new MenuItem("Info") {
+ onAction = (ae) => callback_pkg_info(row.item.value.name.value)
+ }
+ val installMI = new MenuItem("Install") {
+ onAction = (ae) => callback_install(row.item.value.name.value)
+ }
+ val removeMI = new MenuItem("Remove") {
+ onAction = (ae) => callback_remove(row.item.value.name.value)
+ }
+ val ctm = new ContextMenu(infoMI, installMI, removeMI)
+ row.item.onChange { (_,_,newTL) =>
+ if (newTL != null) {
+ val is_installed: Boolean = !(newTL.installed.value == "Not installed")
+ installMI.disable = is_installed
+ removeMI.disable = !is_installed
+ }
+ }
+ row.contextMenu = ctm
+ row
+ }
+ table
+ }
+ val backupTable: TreeTableView[TLBackupDisplay] = {
+ val colName = new TreeTableColumn[TLBackupDisplay, String] {
+ text = "Package"
+ cellValueFactory = { _.value.value.value.name }
+ prefWidth = 150
+ }
+ val colRev = new TreeTableColumn[TLBackupDisplay, String] {
+ text = "Revision"
+ cellValueFactory = { _.value.value.value.rev }
+ prefWidth = 100
+ }
+ val colDate = new TreeTableColumn[TLBackupDisplay, String] {
+ text = "Date"
+ cellValueFactory = { _.value.value.value.date }
+ prefWidth = 300
+ }
+ val table = new TreeTableView[TLBackupDisplay](
+ new TreeItem[TLBackupDisplay](new TLBackupDisplay("root","","")) {
+ expanded = false
+ }) {
+ columns ++= List(colName, colRev, colDate)
+ }
+ colDate.prefWidth.bind(table.width - colRev.width - colName.width - 15)
+ table.prefHeight = 300
+ table.showRoot = false
+ table.vgrow = Priority.Always
+ table.placeholder = new Label("No backups available") {
+ opacity = 0.4f
+ font = msgFont
+ }
+ table.rowFactory = { _ =>
+ val row = new TreeTableRow[TLBackupDisplay] {}
+ val ctm = new ContextMenu(
+ new MenuItem("Info") {
+ onAction = (ae) => callback_pkg_info(row.item.value.name.value)
+ },
+ new MenuItem("Restore") {
+ onAction = (ae) => callback_restore(row.item.value.name.value, row.item.value.rev.value)
+ }
+ )
+ row.contextMenu = ctm
+ row
+ }
+ table
+ }
+ val searchEntry = new TextField()
+ searchEntry.hgrow = Priority.Sometimes
+ searchEntry.onKeyPressed = {
+ (ae: KeyEvent) => if (ae.code == KeyCode.Enter) update_pkgs_view()
+ }
+ val statusLabel = new Label("Idle")
+ val actionLabel = new Label("") {
+ hgrow = Priority.Always
+ maxWidth = Double.MaxValue
+ }
+ val statusBox = new HBox {
+ children = Seq(
+ new Label("Tlmgr status:") {
+ vgrow = Priority.Always
+ alignmentInParent = Pos.CenterLeft
+ },
+ statusLabel,
+ actionLabel
+ )
+ maxWidth = Double.MaxValue
+ hgrow = Priority.Always
+ alignment = Pos.Center
+ alignmentInParent = Pos.CenterLeft
+ padding = Insets(10)
+ spacing = 10
+ }
+
+ val searchBox = new HBox {
+ children = Seq(
+ new Label("Search:") {
+ vgrow = Priority.Always
+ alignmentInParent = Pos.CenterLeft
+ },
+ searchEntry,
+ new Button("Go") {
+ onAction = _ => update_pkgs_view()
+ },
+ new Button("Reset") {
+ onAction = _ => {
+ searchEntry.text = ""
+ update_pkgs_view()
+ }
+ }
+ )
+ alignment = Pos.Center
+ // do not add padding at the bottom as we have one from the status field
+ padding = Insets(10,10,0,10)
+ spacing = 10
+ }
+ val pkgsContainer = new VBox {
+ children = Seq(packageTable,searchBox)
+ }
+
+ val pkgstabs: TabPane = new TabPane {
+ minWidth = 400
+ vgrow = Priority.Always
+ tabs = Seq(
+ new Tab {
+ text = "Packages"
+ closable = false
+ content = pkgsContainer
+ },
+ new Tab {
+ text = "Updates"
+ closable = false
+ content = updateTable
+ },
+ new Tab {
+ text = "Backups"
+ closable = false
+ content = backupTable
+ }
+ )
+ }
+ // val spacerMenu: Menu = new Menu(" ")
+ // spacerMenu.disable = true
+ // spacerMenu.hgrow = Priority.Always
+ val menuBar: MenuBar = new MenuBar {
+ useSystemMenuBar = true
+ // menus.addAll(mainMenu, optionsMenu, helpMenu)
+ menus.addAll(mainMenu, pkgsMenu, toolsMenu, optionsMenu)
+ }
+ var updLoaded = false
+ var bckLoaded = false
+ pkgstabs.selectionModel().selectedItem.onChange(
+ (a,b,c) => {
+ if (a.value.text() == "Backups") {
+ if (!bckLoaded) {
+ load_backups_update_bkps_view()
+ bckLoaded = true
+ }
+ menuBar.menus = Seq(mainMenu, toolsMenu, optionsMenu)
+ } else if (a.value.text() == "Updates") {
+ // only update if not done already
+ if (!updLoaded) {
+ load_updates_update_upds_view()
+ updLoaded = true
+ }
+ menuBar.menus = Seq(mainMenu, updMenu, toolsMenu, optionsMenu)
+ } else if (a.value.text() == "Packages") {
+ menuBar.menus = Seq(mainMenu, pkgsMenu, toolsMenu, optionsMenu)
+ }
+ }
+ )
+
+
+ stage = new PrimaryStage {
+ title = "TLCockpit"
+ scene = new Scene {
+ root = {
+ // val topBox = new HBox {
+ // children = List(menuBar, statusLabel)
+ // }
+ // topBox.hgrow = Priority.Always
+ // topBox.maxWidth = Double.MaxValue
+ val topBox = menuBar
+ val centerBox = new VBox {
+ padding = Insets(10)
+ children = List(pkgstabs, statusBox, expertPane, outerrpane)
+ }
+ new BorderPane {
+ // padding = Insets(20)
+ top = topBox
+ // left = leftBox
+ center = centerBox
+ // bottom = bottomBox
+ }
+ }
+ }
+ icons.add(iconImage)
+ }
+
+ stage.onCloseRequest = (e: Event) => callback_quit()
+
+ stage.width = 800
+
+
+ var currentPromise = Promise[(String,Array[String])]()
+ val pendingJobs = scala.collection.mutable.Queue[(String,(String, Array[String]) => Unit)]()
+
+ def initialize_tlmgr(): TlmgrProcess = {
+ tlmgrBusy.value = true
+ actionLabel.text = "[initializing tlmgr]"
+ // create new sync vars for each process
+ val outputLine = new SyncVar[String]
+ val errorLine = new SyncVar[String]
+
+ val tt = new TlmgrProcess(
+ (s: String) => {
+ logger.trace(s"outputline put ${s}")
+ outputLine.put(s)
+ },
+ (s: String) => {
+ logger.trace(s"errorline put ${s}")
+ errorLine.put(s)
+ }
+ )
+ if (!tt.start_process()) {
+ logger.debug("Cannot start tlmgr process, terminating!")
+ Platform.exit()
+ sys.exit(1)
+ }
+ logger.debug("initialize_tlmgr: sleeping after starting process")
+ Thread.sleep(1000)
+ /* val tlmgrMonitor = Future {
+ while (true) {
+ if (!tlmgr.isAlive) {
+ logger.debug("TLMGR HAS DIED!!!!")
+ // Platform.exit()
+ // sys.exit(1)
+ }
+ Thread.sleep(5000)
+ }
+ }*/
+ val stdoutFuture = Future {
+ val tlmgrOutput = ArrayBuffer[String]()
+ var tlmgrStatus = ""
+ var alive = true
+ logger.debug("initialize tlmgr: starting stdout reader thread")
+ while (alive) {
+ logger.trace("stdout reader before outputLine.take")
+ val s = outputLine.take
+ logger.trace("stdout reader after outputLine.take")
+ if (s == null) {
+ alive = false
+ logger.debug("got null from stdout, tlmgr seems to be terminated for restart")
+ if (!currentPromise.isCompleted) {
+ logger.debug(s"Fulfilling remaining open promise with ${tlmgrStatus} and ${tlmgrOutput.toArray.mkString}")
+ currentPromise.success((tlmgrStatus, tlmgrOutput.toArray))
+ }
+ tlmgrStatus = ""
+ tlmgrOutput.clear()
+ tlmgrBusy.value = false
+ } else {
+ logger.trace(s"DEBUG: got ==" + s + "==")
+ if (s == "OK") {
+ tlmgrStatus = s
+ } else if (s == "ERROR") {
+ tlmgrStatus = s
+ } else if (s == "tlmgr> ") {
+ logger.trace(s"Fulfilling current promise with ${tlmgrStatus} and ${tlmgrOutput.toArray.mkString}!")
+ if (!currentPromise.isCompleted) {
+ currentPromise.success((tlmgrStatus, tlmgrOutput.toArray))
+ }
+ tlmgrStatus = ""
+ tlmgrOutput.clear()
+ tlmgrBusy.value = false
+ if (pendingJobs.nonEmpty) {
+ logger.debug("pending Job found!")
+ val nextCmd = pendingJobs.dequeue()
+ logger.debug(s"running ${nextCmd._1}")
+ tlmgr_run_one_cmd(nextCmd._1, nextCmd._2)
+ }
+ } else {
+ tlmgrOutput += s
+ stdoutLineUpdateFunc(s)
+ }
+ }
+ }
+ logger.debug("initialize tlmgr: finishing stdout reader thread")
+ }
+ tlmgrBusy.onChange({
+ Platform.runLater {
+ statusLabel.text = if (tlmgrBusy.value) "Busy" else "Idle"
+ if (!tlmgrBusy.value)
+ actionLabel.text = ""
+ }
+ })
+
+ stdoutFuture.onComplete {
+ case Success(value) =>
+ logger.debug(s"tlmgr stdout reader terminated: ${value}")
+ case Failure(e) =>
+ logger.debug(s"tlmgr stdout reader terminated with error: ${e}")
+ }
+
+ val stderrFuture = Future {
+ var alive = true
+ logger.debug("initialize tlmgr: starting stderr reader thread")
+ while (alive) {
+ val s = errorLine.take
+ if (s == null)
+ alive = false
+ else
+ stderrLineUpdateFunc(s)
+ }
+ logger.debug("initialize tlmgr: finishing stderr reader thread")
+ }
+ stderrFuture.onComplete {
+ case Success(value) =>
+ logger.debug(s"tlmgr stderr reader terminated: ${value}")
+ case Failure(e) =>
+ logger.debug(s"tlmgr stderr reader terminated with failure: ${e}")
+ }
+
+ tt
+ }
+
+ def tlmgr_post_init() = {
+
+ // check for tlmgr revision
+ tlmgr_send("version", (status,output) => {
+ logger.debug(s"Callback after version, got ${status} and ${output.mkString("\n")}")
+ output.foreach ( l => {
+ if (l.startsWith("revision ")) {
+ val tlmgrRev = l.stripPrefix("revision ")
+ if (tlmgrRev == "unknown") {
+ logger.debug("Unknown tlmgr revision, assuming git/svn version")
+ logText += "Unknown tlmgr revision, assuming git/svn version"
+ } else {
+ if (tlmgrRev.toInt < 45838) {
+ new Alert(AlertType.Error) {
+ initOwner(stage)
+ title = "TeX Live Manager tlmgr is too old"
+ headerText = "TeX Live Manager tlmgr is too old"
+ contentText = "Please update from the command line\nusing 'tlmgr update --self'\nTerminating!"
+ }.showAndWait()
+ callback_quit()
+ }
+ }
+ }
+ })
+ pkgs.clear()
+ upds.clear()
+ bkps.clear()
+ logger.debug("Before loading tlpdb")
+ load_tlpdb_update_pkgs_view_no_json()
+ logger.debug("after loading tlpdb")
+ })
+ }
+
+ def tlmgr_run_one_cmd(s: String, onCompleteFunc: (String, Array[String]) => Unit): Unit = {
+ currentPromise = Promise[(String, Array[String])]()
+ tlmgrBusy.value = true
+ Platform.runLater {
+ actionLabel.text = s"[${s}]"
+ }
+ currentPromise.future.onComplete {
+ case Success((a, b)) =>
+ logger.debug("tlmgr run one cmd: current future completed!")
+ Platform.runLater {
+ logger.debug("tlmgr run one cmd: running on complete function")
+ onCompleteFunc(a, b)
+ }
+ case Failure(ex) =>
+ logger.debug("Running tlmgr command did no succeed" + ex.getMessage)
+ errorText += "Running a tlmgr command did not succeed: " + ex.getMessage
+ }
+ logger.debug(s"sending to tlmgr: ${s}")
+ tlmgr.send_command(s)
+ }
+
+ def tlmgr_send(s: String, onCompleteFunc: (String, Array[String]) => Unit): Unit = {
+ // logText.clear()
+ outputText.clear()
+ // don't close debug panel when it is open
+ // outerrpane.expanded = false
+ if (!currentPromise.isCompleted) {
+ logger.debug(s"tlmgr busy, put onto pending jobs: $s")
+ logger.debug("Currently running job: " + currentPromise)
+ pendingJobs += ((s, onCompleteFunc))
+ } else {
+ logger.debug(s"tlmgr_send sending ${s}")
+ tlmgr_run_one_cmd(s, onCompleteFunc)
+ }
+ }
+
+ def reinitialize_tlmgr(): Unit = {
+ logger.debug("reinit tlmgr: entering, clearing pending jobs")
+ pendingJobs.clear()
+ logger.debug("reinit tlmgr: cleared pending jobs")
+ // if (!currentPromise.isCompleted) {
+ // logger.debug("reinit tlmgr: current promise not complete, completing it")
+ // currentPromise.success(("",Array[String]()))
+ // logger.debug("reinit tlmgr: after completing current promise")
+ // }
+ logger.debug("reinit tlmgr: cleaning up tlmgr")
+ tlmgr.cleanup()
+ logger.debug("reinit tlmgr: sleeping 1s")
+ Thread.sleep(1000)
+ logger.debug("reinit tlmgr: initializaing new tlmgr")
+ tlmgr = initialize_tlmgr()
+ logger.debug("reinit tlmgr: finished")
+ tlmgr_post_init()
+ logger.debug("reinit tlmgr: post init done")
+ updLoaded = false
+ pkgstabs.getSelectionModel.select(0)
+ }
+
+ def defaultStdoutLineUpdateFunc(l: String) : Unit = { logger.trace(s"DEBUG: got ==$l== from tlmgr") }
+ def defaultStderrLineUpdateFunc(l: String) : Unit = { Platform.runLater { logText.append(l) } }
+
+ var stdoutLineUpdateFunc: String => Unit = defaultStdoutLineUpdateFunc
+ var stderrLineUpdateFunc: String => Unit = defaultStderrLineUpdateFunc
+
+
+ var tlmgr = initialize_tlmgr()
+ tlmgr_post_init()
+
+} // object ApplicationMain
+
+// vim:set tabstop=2 expandtab : //
diff --git a/support/tlcockpit/src/main/scala/TLCockpit/LocationDialog.scala b/support/tlcockpit/src/main/scala/TLCockpit/LocationDialog.scala
new file mode 100644
index 0000000000..647e0cc038
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TLCockpit/LocationDialog.scala
@@ -0,0 +1,140 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package TLCockpit
+
+import TLCockpit.ApplicationMain.stage
+import TeXLive.{OsTools, TLOption}
+import com.typesafe.scalalogging.LazyLogging
+
+import scalafx.Includes._
+import scalafx.event.ActionEvent
+import scalafx.geometry.{HPos, Insets}
+import scalafx.scene.Node
+import scalafx.scene.control._
+import scalafx.scene.layout._
+
+class LocationDialog(locs: Map[String,String]) extends LazyLogging {
+
+ case class Result(selected: Map[String, String])
+
+ val dialog = new Dialog[Result]() {
+ initOwner(stage)
+ title = s"Default package repositories"
+ headerText = s"Default package repositories"
+ resizable = true
+ }
+ dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel)
+ val grid = new GridPane() {
+ hgap = 10
+ vgap = 10
+ padding = Insets(20)
+ }
+
+ val newlocs = scala.collection.mutable.ArrayBuffer[(TextField, TextField)]()
+
+ var crow = 0
+
+ val mirrorlocs: Map[String, Map[String, Seq[String]]] = TLCockpit.ApplicationMain.parse_ctan_mirrors(TLCockpit.ApplicationMain.tlmgr.tlroot)
+
+ def do_one(tag: String, url: String): Unit = {
+ val tagNode = new TextField() {
+ text = tag
+ }
+ tagNode.disable = (tag == "main")
+ val urlNode = new TextField() {
+ text = url
+ }
+ grid.add(tagNode, 0, crow)
+ grid.add(urlNode, 1, crow)
+ newlocs += ((tagNode, urlNode))
+ if (tag == "main") {
+ if (mirrorlocs.nonEmpty) {
+ grid.add(new MenuButton("Choose Mirror") {
+ items = mirrorlocs.toSeq.sortWith(_._1 < _._1)
+ .map(continent_pair => new Menu(continent_pair._1) {
+ items = continent_pair._2.toSeq.sortWith(_._1 < _._1)
+ .map(country_pair => new Menu(country_pair._1) {
+ items = country_pair._2.sortWith(_ < _).map(mirror => new MenuItem(mirror) {
+ onAction = (ae) => urlNode.text = mirror
+ })
+ })
+ })
+ }, 2, crow)
+ }
+ } else {
+ grid.add(new Button("Delete") {
+ onAction = _ => {
+ tagNode.disable = true
+ urlNode.disable = true
+ this.disable = true
+ }
+ }, 2, crow)
+ }
+ crow += 1
+ }
+
+ do_one("main", locs("main"))
+ locs.filter(_._1 != "main").foreach(p => do_one(p._1, p._2))
+
+ val addButton = new Button("Add")
+ addButton.onAction = _ => {
+ val tagNode = new TextField()
+ val urlNode = new TextField()
+ newlocs += ((tagNode, urlNode))
+ logger.trace(s"current row = ${crow}")
+ grid.children.remove(addButton)
+ grid.add(tagNode, 0, crow)
+ grid.add(urlNode, 1, crow)
+ grid.add(new Button("Delete") {
+ onAction = _ => {
+ tagNode.disable = true
+ urlNode.disable = true
+ this.disable = true
+ }
+ }, 2, crow)
+ crow += 1
+ logger.trace(s"adding addButton at row ${crow}")
+ grid.add(addButton, 2, crow)
+ dialog.dialogPane.value.scene.value.window.value.sizeToScene()
+ }
+ grid.add(addButton, 2, crow)
+
+ // val changedValues = scala.collection.mutable.Map[String,String]()
+
+
+ grid.columnConstraints = Seq(new ColumnConstraints(100, 100, 200), new ColumnConstraints(250, 250, 5000, Priority.Always, HPos.LEFT, true))
+ dialog.dialogPane().content = grid
+ dialog.width = 500
+ dialog.height = 1500
+
+ dialog.resultConverter = dialogButton =>
+ if (dialogButton == ButtonType.OK) {
+ val newlocsfiltered =
+ newlocs.filter(p => (p._1.text.value + p._2.text.value).trim != "")
+ .filter(p => !p._2.disabled.value)
+ .map(p => (if (p._1.text.value == "") p._2.text.value else p._1.text.value, p._2.text.value))
+ .toMap
+ Result(newlocsfiltered)
+ } else
+ null
+
+ def showAndWait(): Option[Map[String,String]] = {
+ val result = dialog.showAndWait()
+
+ result match {
+ case Some(Result(foo)) =>
+ logger.debug("Got result " + foo)
+ Some(foo)
+ case Some(foo) =>
+ logger.debug("Got strange result " + foo)
+ None
+ case None =>
+ None
+ }
+ }
+
+}
diff --git a/support/tlcockpit/src/main/scala/TLCockpit/OptionsDialog.scala b/support/tlcockpit/src/main/scala/TLCockpit/OptionsDialog.scala
new file mode 100644
index 0000000000..52a9d5d0ce
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TLCockpit/OptionsDialog.scala
@@ -0,0 +1,136 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package TLCockpit
+
+import TLCockpit.ApplicationMain.{not_implemented_info, stage}
+import TeXLive.{OsTools, TLOption, TLPaperConf}
+import com.typesafe.scalalogging.LazyLogging
+
+import scalafx.Includes._
+import scalafx.collections.ObservableBuffer
+import scalafx.event.ActionEvent
+import scalafx.geometry.{HPos, Insets}
+import scalafx.scene.Node
+import scalafx.scene.control._
+import scalafx.scene.layout._
+
+class OptionsDialog(opts: List[TLOption]) extends LazyLogging {
+
+ case class Result(selected: Map[String,String])
+
+ val dialog = new Dialog[Result]() {
+ initOwner(stage)
+ title = s"General options"
+ headerText = s"General options"
+ resizable = true
+ }
+ dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel)
+ val grid = new GridPane() {
+ hgap = 10
+ vgap = 10
+ padding = Insets(20)
+ }
+
+ var crow = 0
+ val optsMap = opts.map(p => (p.name, p)).toMap
+ val orderOpts = Seq("location", "create_formats", "install_srcfiles", "install_docfiles",
+ "backupdir", "autobackup") ++ {
+ if (OsTools.isWindows) Seq("desktop_integration", "file_assocs", "w32_multi_user")
+ else Seq("sys_bin", "sys_info", "sys_man")
+ }
+ val changedValues = scala.collection.mutable.Map[String,String]()
+ val nodes = orderOpts.map( nm => {
+ val tlopt = optsMap(nm) // this is dangerous in case we change the names in TLPDB!
+ grid.add(new Label(tlopt.description), 0, crow)
+ val value = tlopt.value.getOrElse(tlopt.default)
+ val nd: Node = if (tlopt.format.startsWith("b"))
+ new CheckBox() {
+ selected = if (value == "1") true else false
+ selected.onChange( (_,_,newVal) => { changedValues(nm) = if (newVal) "1" else "0" })
+ }
+ else if (tlopt.format.startsWith("n")) {
+ // parse additional specs
+ if (tlopt.format.startsWith("n:")) {
+ var nrSpec = tlopt.format.substring(2)
+ var splits: Array[String] = nrSpec.split("""\.\.""")
+ val start: Int = splits(0).toInt
+ val end: Option[Int] = if (splits.length > 1) Some(splits(1).toInt) else None
+ new Spinner[Int](start, end.getOrElse(50), value.toInt) {
+ value.onChange( (_,_,newVal) => { changedValues(nm) = newVal.toString })
+ if (nm == "autobackup")
+ tooltip = new Tooltip("-1 ... arbitrary many backups\n0 ... no backups\notherwise number of backups")
+ }
+ } else {
+ new Spinner[Int](-50, 50, value.toInt) {
+ value.onChange( (_,_,newVal) => { changedValues(nm) = newVal.toString })
+ }
+ }
+ } else if (tlopt.format == "p")
+ new TextField() {
+ text = value
+ text.onChange( (_,_,newVal) => { changedValues(nm) = newVal })
+ }
+ else if (tlopt.format == "u") {
+ val locs = value.split(' ')
+ val locsmap = if (locs.length == 1) Map[String,String](("main", value))
+ else locs.map(p => {
+ val uu = p.split('#')
+ (uu(1), uu(0))
+ }).toMap
+ val buttonStr = if (locs.length == 1) value else "Multiple repository"
+ new Button(buttonStr) {
+ onAction = (event: ActionEvent) => {
+ val dg = new LocationDialog(locsmap)
+ dg.showAndWait() match {
+ case Some(newlocs) =>
+ if (newlocs.toList.length == 1) {
+ changedValues(nm) = newlocs("main")
+ this.text = newlocs("main")
+ } else {
+ changedValues(nm) = newlocs.map(p => p._2 + "#" + p._1).mkString(" ")
+ this.text = "Multiple repository"
+ }
+ case None =>
+ }
+ }
+ }
+ } else
+ new Label("Unknown setting")
+
+ grid.add(nd, 1, crow)
+ crow += 1
+ (tlopt.name, nd)
+ }).toMap
+
+ grid.columnConstraints = Seq(new ColumnConstraints(300, 300, 500), new ColumnConstraints(200, 200, 5000, Priority.Always, HPos.LEFT, true))
+ dialog.dialogPane().content = grid
+ dialog.width = 500
+ dialog.height = 1500
+
+ dialog.resultConverter = dialogButton =>
+ if (dialogButton == ButtonType.OK) {
+ // TODO we could check that the values actually have changed
+ Result(changedValues.toMap)
+ } else
+ null
+
+ def showAndWait(): Option[Map[String,String]] = {
+ val result = dialog.showAndWait()
+
+ result match {
+ case Some(Result(foo)) =>
+ logger.debug("Got result " + foo)
+ Some(foo)
+ case Some(foo) =>
+ logger.debug("Got strange result " + foo)
+ None
+ case None =>
+ None
+ }
+ }
+
+}
diff --git a/support/tlcockpit/src/main/scala/TLCockpit/PaperDialog.scala b/support/tlcockpit/src/main/scala/TLCockpit/PaperDialog.scala
new file mode 100644
index 0000000000..3d7a2e8403
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TLCockpit/PaperDialog.scala
@@ -0,0 +1,90 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package TLCockpit
+
+import TLCockpit.ApplicationMain.stage
+import TeXLive.TLPaperConf
+import com.typesafe.scalalogging.LazyLogging
+
+import scalafx.Includes._
+import scalafx.collections.ObservableBuffer
+import scalafx.geometry.{HPos, Insets}
+import scalafx.scene.control._
+import scalafx.scene.layout._
+
+class PaperDialog(paperconf: Map[String, TLPaperConf]) extends LazyLogging {
+
+ case class Result(selected: Map[String,String])
+
+ val dialog = new Dialog[Result]() {
+ initOwner(stage)
+ title = s"Paper configuration"
+ headerText = s"Paper configuration"
+ resizable = true
+ }
+ dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel)
+ val grid = new GridPane() {
+ hgap = 10
+ vgap = 10
+ padding = Insets(20)
+ }
+
+ var crow = 0
+
+ def update_all_to(str: String): Unit = {
+ cbseq.foreach(f => f._2.value = str)
+ }
+ grid.add(new Label("Set all to:"), 0, crow)
+ grid.add(new HBox(20) {
+ children = List(
+ new Button("a4") { onAction = _ => update_all_to("a4") },
+ new Button("letter") { onAction = _ => update_all_to("letter")}
+ )}, 1, crow)
+ crow += 1
+ def do_one(k: String, v: ChoiceBox[String], row: Int): Int = {
+ grid.add(new Label(k), 0, row)
+ grid.add(v, 1, row)
+ row + 1
+ }
+
+ val cbseq: Map[String, ChoiceBox[String]] = paperconf.map { p =>
+ val prog: String = p._1
+ val opts: TLPaperConf = p._2
+ val paperopts: Seq[String] = opts.options
+ val cb = new ChoiceBox[String](ObservableBuffer(paperopts.sorted))
+ cb.value = paperopts.head
+ crow = do_one(prog,cb, crow)
+ (prog, cb)
+ }
+
+ grid.columnConstraints = Seq(new ColumnConstraints(100, 100, 150), new ColumnConstraints(150, 150, 5000, Priority.Always, HPos.LEFT, true))
+ dialog.dialogPane().content = grid
+ dialog.width = 300
+ dialog.height = 1500
+
+ dialog.resultConverter = dialogButton =>
+ if (dialogButton == ButtonType.OK)
+ Result(cbseq.mapValues(_.value.value))
+ else
+ null
+
+ def showAndWait(): Option[Map[String,String]] = {
+ val result = dialog.showAndWait()
+
+ result match {
+ case Some(Result(foo)) =>
+ logger.debug("Got result " + foo)
+ Some(foo)
+ case Some(foo) =>
+ logger.debug("Got strange result " + foo)
+ None
+ case None =>
+ None
+ }
+ }
+
+}
diff --git a/support/tlcockpit/src/main/scala/TLCockpit/PkgInfoDialog.scala b/support/tlcockpit/src/main/scala/TLCockpit/PkgInfoDialog.scala
new file mode 100644
index 0000000000..89ba164de2
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TLCockpit/PkgInfoDialog.scala
@@ -0,0 +1,174 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package TLCockpit
+
+import TLCockpit.ApplicationMain.{stage, tlmgr, tlpkgs}
+import TLCockpit.Utils._
+import TeXLive.{OsTools, TLPackage}
+
+import scalafx.Includes._
+import scalafx.collections.ObservableBuffer
+import scalafx.geometry.{HPos, Insets, Orientation}
+import scalafx.scene.Cursor
+import scalafx.scene.layout.{ColumnConstraints, GridPane, Priority, VBox}
+import scalafx.scene.control._
+import scalafx.scene.input.MouseEvent
+import scalafx.scene.paint.Color
+
+class PkgInfoDialog(tlp: TLPackage) extends Dialog {
+ val dialog = new Dialog() {
+ initOwner(stage)
+ title = s"Package Information for $pkg"
+ headerText = s"Package Information for $pkg"
+ resizable = true
+ }
+ dialog.dialogPane().buttonTypes = Seq(ButtonType.OK)
+ val isInstalled = tlp.installed
+ val grid = new GridPane() {
+ hgap = 10
+ vgap = 10
+ padding = Insets(20)
+ }
+
+ def copyableTextfield(k: String): TextField = {
+ new TextField() {
+ text = k
+ editable = false
+ style = "-fx-background-color: rgba(0, 0, 0, 0);"
+ margin = Insets(0)
+ padding = Insets(0)
+ }
+ }
+
+ def do_one(k: String, v: String, row: Int): Int = {
+ grid.add(copyableTextfield(k), 0, row)
+ grid.add(copyableTextfield(v), 1, row)
+ // grid.add(new Label(k), 0, row)
+ // grid.add(new Label(v) {
+ // wrapText = true
+ // }, 1, row)
+ row + 1
+ }
+
+ var crow = 0
+ val pkg = tlp.name
+ crow = do_one("package", pkg, crow)
+ crow = do_one("category", tlp.category, crow)
+ crow = do_one("shortdesc", tlp.shortdesc.getOrElse(""), crow)
+ crow = do_one("longdesc", tlp.longdesc.getOrElse(""), crow)
+ crow = do_one("installed", if (tlp.installed) "Yes" else "No", crow)
+ crow = do_one("available", if (tlp.available) "Yes" else "No", crow)
+ if (tlp.installed)
+ crow = do_one("local revision", tlp.lrev.toString, crow)
+ if (tlp.available)
+ crow = do_one("remote revision", tlp.rrev.toString, crow)
+ val binsizesum = tlp.binsize.values.toList.sum * TeXLive.tlBlockSize
+ val binsizestr = if (binsizesum > 0) "bin " + humanReadableByteSize(binsizesum) + " " else ""
+ val runsizestr = if (tlp.runsize > 0) "run " + humanReadableByteSize(tlp.runsize) + " " else ""
+ val srcsizestr = if (tlp.srcsize > 0) "src " + humanReadableByteSize(tlp.srcsize) + " " else ""
+ val docsizestr = if (tlp.docsize > 0) "doc " + humanReadableByteSize(tlp.docsize) + " " else ""
+ crow = do_one("sizes", runsizestr + docsizestr + binsizestr + srcsizestr, crow)
+ val catdata = tlp.cataloguedata
+ if (catdata.version.isDefined)
+ crow = do_one("cat-version", catdata.version.get, crow)
+ if (catdata.date.isDefined)
+ crow = do_one("cat-date", catdata.date.get, crow)
+ if (catdata.license.isDefined)
+ crow = do_one("cat-license", catdata.license.get, crow)
+ if (catdata.topics.isDefined)
+ crow = do_one("cat-topics", catdata.topics.get, crow)
+ if (catdata.related.isDefined)
+ crow = do_one("cat-related", catdata.related.get, crow)
+ // add files section
+ //println(tlpkgs(pkg))
+ val docFiles = tlp.docfiles
+ if (docFiles.nonEmpty) {
+ // grid.add(new Label("doc files"), 0, crow)
+ grid.add(copyableTextfield("doc files"), 0, crow)
+ grid.add(doListView(docFiles.map(s => s.file.replaceFirst("RELOC", "texmf-dist")), isInstalled), 1, crow)
+ crow += 1
+ }
+ val runFiles = tlp.runfiles
+ if (runFiles.nonEmpty) {
+ // grid.add(new Label("run files"), 0, crow)
+ grid.add(copyableTextfield("run files"), 0, crow)
+ grid.add(doListView(runFiles.map(s => s.replaceFirst("RELOC", "texmf-dist")), false), 1, crow)
+ crow += 1
+ }
+ val srcFiles = tlp.srcfiles
+ if (srcFiles.nonEmpty) {
+ // grid.add(new Label("src files"), 0, crow)
+ grid.add(copyableTextfield("src files"), 0, crow)
+ grid.add(doListView(srcFiles.map(s => s.replaceFirst("RELOC", "texmf-dist")), false), 1, crow)
+ crow += 1
+ }
+ val binFiles = tlp.binfiles
+ if (binFiles.nonEmpty) {
+ // grid.add(new Label("bin files"), 0, crow)
+ grid.add(copyableTextfield("bin files"), 0, crow)
+ grid.add(doListView(binFiles.flatMap(_._2).toSeq.map(s => s.replaceFirst("RELOC", "texmf-dist")), false), 1, crow)
+ crow += 1
+ }
+ grid.columnConstraints = Seq(new ColumnConstraints(100, 200, 200), new ColumnConstraints(100, 400, 5000, Priority.Always, HPos.LEFT, true))
+ dialog.dialogPane().content = grid
+ dialog.width = 600
+ dialog.height = 1500
+ // dialog
+
+ def showAndWait(): Unit = this.dialog.showAndWait()
+
+ def doListView(files: Seq[String], clickable: Boolean): scalafx.scene.Node = {
+ if (files.length <= 5) {
+ val vb = new VBox()
+ vb.children = files.map { f =>
+ val fields = f.split(" ")
+ val foo = copyableTextfield(fields(0))
+ if (clickable) {
+ foo.style = foo.getStyle + "-fx-text-fill: blue;"
+ foo.onMouseClicked = { me: MouseEvent => OsTools.openFile(tlmgr.tlroot + "/" + fields(0)) }
+ foo.cursor = Cursor.Hand
+ }
+ foo
+ // new Label(fields(0)) {
+ // if (clickable) {
+ // textFill = Color.Blue
+ // onMouseClicked = { me: MouseEvent => OsTools.openFile(tlmgr.tlroot + "/" + fields(0)) }
+ // cursor = Cursor.Hand
+ // }
+ // }
+ }
+ vb
+ } else {
+ val vb = new ListView[String] {}
+ vb.minHeight = 100
+ vb.prefHeight = 100
+ vb.maxHeight = 200
+ vb.vgrow = Priority.Always
+ vb.orientation = Orientation.Vertical
+ vb.cellFactory = { p => {
+ val foo = new ListCell[String]
+ // ensure tighter spacing similar to the other entires
+ foo.padding = Insets(0)
+ foo.item.onChange { (_, _, str) => foo.text = str }
+ if (clickable) {
+ foo.textFill = Color.Blue
+ foo.onMouseClicked = { me: MouseEvent => OsTools.openFile(tlmgr.tlroot + "/" + foo.text.value) }
+ foo.cursor = Cursor.Hand
+ }
+ foo
+ }
+ }
+ // vb.children = docFiles.map { f =>
+ vb.items = ObservableBuffer(files.map { f =>
+ val fields = f.split(" ")
+ fields(0)
+ })
+ vb
+ }
+ }
+
+}
diff --git a/support/tlcockpit/src/main/scala/TLCockpit/TLDisplayClasses.scala b/support/tlcockpit/src/main/scala/TLCockpit/TLDisplayClasses.scala
new file mode 100644
index 0000000000..b49c13c162
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TLCockpit/TLDisplayClasses.scala
@@ -0,0 +1,36 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package TLCockpit
+
+import scalafx.beans.property.{ObjectProperty, StringProperty}
+
+
+// Note!!! we have to use ObjectProperty[Int] here instead of IntegerProperty
+// since IntegerProperty does NOT implement Observable[Int,Int]
+// see https://github.com/scalafx/scalafx/issues/243
+case class TLPackageDisplay(name: StringProperty, var lrev: ObjectProperty[Int], rrev: ObjectProperty[Int],
+ shortdesc: StringProperty, size: ObjectProperty[Int], var installed: StringProperty) {
+ def this(_name: String, _lrev: String, _rrev: String, _shortdesc: String, _size: String, _installed: String) =
+ this(
+ StringProperty(_name), ObjectProperty[Int](_lrev.toInt), ObjectProperty[Int](_rrev.toInt),
+ StringProperty(_shortdesc), ObjectProperty[Int](_size.toInt), StringProperty(_installed)
+ )
+}
+
+case class TLBackupDisplay(name: StringProperty, rev: StringProperty, date: StringProperty) {
+ def this(_name: String, _rev: String, _date: String) =
+ this(StringProperty(_name), StringProperty(_rev), StringProperty(_date))
+}
+
+case class TLUpdateDisplay(name: StringProperty, var status: StringProperty, var lrev: StringProperty, rrev: StringProperty,
+ shortdesc: StringProperty, size: StringProperty) {
+ def this(_name: String, _status: String, _lrev: String, _rrev: String, _shortdesc: String, _size: String) =
+ this(
+ StringProperty(_name), StringProperty(_status), StringProperty(_lrev), StringProperty(_rrev),
+ StringProperty(_shortdesc), StringProperty(_size)
+ )
+} \ No newline at end of file
diff --git a/support/tlcockpit/src/main/scala/TLCockpit/Utils.scala b/support/tlcockpit/src/main/scala/TLCockpit/Utils.scala
new file mode 100644
index 0000000000..0753e127c6
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TLCockpit/Utils.scala
@@ -0,0 +1,32 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package TLCockpit
+
+object Utils {
+ /**
+ * @see https://stackoverflow.com/questions/3263892/format-file-size-as-mb-gb-etc
+ * @see https://en.wikipedia.org/wiki/Zettabyte
+ * @param fileSize Up to Exabytes
+ * @return
+ */
+ def humanReadableByteSize(fileSize: Long): String = {
+ if(fileSize <= 0) return "0 B"
+ // kilo, Mega, Giga, Tera, Peta, Exa, Zetta, Yotta
+ val units: Array[String] = Array("B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
+ val digitGroup: Int = (Math.log10(fileSize)/Math.log10(1024)).toInt
+ f"${fileSize/Math.pow(1024, digitGroup)}%3.1f ${units(digitGroup)}"
+ }
+
+ def toInt(s: String): Option[Int] = {
+ try {
+ Some(s.toInt)
+ } catch {
+ case e: Exception => None
+ }
+ }
+
+}
diff --git a/support/tlcockpit/src/main/scala/TeXLive/JsonProtocol.scala b/support/tlcockpit/src/main/scala/TeXLive/JsonProtocol.scala
new file mode 100644
index 0000000000..2302d308bd
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TeXLive/JsonProtocol.scala
@@ -0,0 +1,22 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package TeXLive
+
+import spray.json._
+
+
+object JsonProtocol extends DefaultJsonProtocol {
+ implicit val catalogueDataFormat = jsonFormat6(CatalogueData)
+ implicit val docfileFormat = jsonFormat3(DocFile)
+ implicit val tlpackageshortFormat = jsonFormat8(TLPackageShort)
+ implicit val tlpackageFormat = jsonFormat19(TLPackage)
+ implicit val tlbackupFormat = jsonFormat3(TLBackup)
+ implicit val tlpaperconfFormat = jsonFormat3(TLPaperConf)
+ implicit val tloptionsFormat = jsonFormat14(TLOptions)
+ implicit val tloptionFormat = jsonFormat6(TLOption)
+}
+
diff --git a/support/tlcockpit/src/main/scala/TeXLive/OsTools.scala b/support/tlcockpit/src/main/scala/TeXLive/OsTools.scala
new file mode 100644
index 0000000000..1e0aa0f03c
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TeXLive/OsTools.scala
@@ -0,0 +1,55 @@
+package TeXLive
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+import scala.concurrent.Future
+import scala.concurrent.ExecutionContext.Implicits.global
+import sys.process._
+
+object OsTools {
+ val OS: String = System.getProperty("os.name").map(_.toLower)
+ val CygwinRootIsSet: Boolean =
+ sys.env.get("CYGWIN_ROOT") match {
+ case Some(_) => true
+ case None => false
+ }
+ def isWindows: Boolean = {
+ OS.startsWith("windows") && !CygwinRootIsSet
+ }
+ def isCygwin: Boolean = {
+ OS.startsWith("windows") && CygwinRootIsSet
+ }
+ def isApple: Boolean = {
+ OS.startsWith("mac os")
+ }
+ def isUnix: Boolean = {
+ !(isWindows || isApple || isCygwin)
+ }
+
+ def openFileCmd(f: String): Seq[String] = {
+ val absf = new java.io.File(f).getCanonicalPath
+ if (isWindows) {
+ Seq("cmd", "/c", "start", absf)
+ } else if (isCygwin) {
+ Seq("cygstart", absf)
+ } else if (isApple) {
+ Seq("open", absf)
+ } else {
+ Seq("xdg-open", absf)
+ }
+ }
+
+ def openFile(f: String): Unit = {
+ val cmd = openFileCmd(f)
+ val bar = Future {
+ try {
+ cmd.!
+ } catch {
+ case e: Exception => println("Cannot run command: " + cmd + " -- " + e + "\n")
+ }
+ }
+ }
+}
diff --git a/support/tlcockpit/src/main/scala/TeXLive/TLClasses.scala b/support/tlcockpit/src/main/scala/TeXLive/TLClasses.scala
new file mode 100644
index 0000000000..e647ef726e
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TeXLive/TLClasses.scala
@@ -0,0 +1,75 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package TeXLive
+
+// classes that wrap up structures in TeX Live
+// all of them have associated JSON parsers
+
+case class CatalogueData(version: Option[String], topics: Option[String], license: Option[String],
+ date: Option[String], related: Option[String], ctan: Option[String])
+
+case class DocFile(file: String, details: Option[String], language: Option[String])
+case class TLPaperConf(program: String, file: String, options: List[String])
+case class TLBackup(name: String, rev: String, date: String)
+
+case class TLPackageShort
+(
+ name: String,
+ shortdesc: Option[String],
+ var lrev: Long,
+ rrev: Long,
+ category: String,
+ depends: List[String],
+ var installed: Boolean,
+ available: Boolean
+)
+
+case class TLPackage
+(
+ name: String,
+ shortdesc: Option[String],
+ longdesc: Option[String],
+ var lrev: Long,
+ rrev: Long,
+ category: String,
+ docfiles: List[DocFile],
+ runfiles: List[String],
+ srcfiles: List[String],
+ binfiles: Map[String,List[String]],
+ docsize: Long,
+ runsize: Long,
+ srcsize: Long,
+ binsize: Map[String,Long],
+ cataloguedata: CatalogueData,
+ depends: List[String],
+ catalogue: Option[String],
+ var installed: Boolean,
+ available: Boolean
+)
+
+case class TLOption (name: String, description: String, format: String,
+ tlmgrname: String, value: Option[String], default: String)
+
+case class TLOptions
+(
+ sys_bin: String,
+ sys_man: String,
+ sys_info: String,
+ backupdir: String,
+ autobackup: Long,
+ install_srcfiles: Boolean,
+ install_docfiles: Boolean,
+ create_formats: Boolean,
+ post_code: Boolean,
+ generate_updmap: Boolean,
+ location: Map[String,String],
+ desktop_integration: Option[Boolean],
+ w32_multi_user: Option[Boolean],
+ file_assocs: Option[Long]
+)
+
+
diff --git a/support/tlcockpit/src/main/scala/TeXLive/TlmgrProcess.scala b/support/tlcockpit/src/main/scala/TeXLive/TlmgrProcess.scala
new file mode 100644
index 0000000000..3040d3abf7
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TeXLive/TlmgrProcess.scala
@@ -0,0 +1,130 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package TeXLive
+
+import java.io._
+
+import TeXLive.OsTools._
+import com.typesafe.scalalogging.LazyLogging
+
+import scala.collection.mutable.ArrayBuffer
+import scala.concurrent.SyncVar
+import scala.sys.process._
+
+class TlmgrProcess(updout: String => Unit, upderr: String => Unit) extends LazyLogging {
+ val inputString = new SyncVar[String] // used for the tlmgr process input
+ // val outputString = new SyncVar[String] // used for the tlmgr process output
+ // val errorBuffer: StringBuffer = new StringBuffer() // buffer used for both tmgr process error console AND logging
+
+ val tlroot: String = "kpsewhich -var-value SELFAUTOPARENT".!!.trim
+ logger.debug("tlroot ==" + tlroot + "==")
+
+ // set in only one place, in the main thread
+ var process: Process = _
+
+ def send_command(input: String): Unit = {
+ logger.debug(s"send_command: ${input}")
+ try {
+ assert(!inputString.isSet)
+ inputString.put(input)
+ } catch {
+ case exc: Throwable =>
+ logger.warn("Main thread: " +
+ (if (exc.isInstanceOf[NoSuchElementException]) "Timeout" else "Exception: " + exc))
+ }
+ }
+
+ def isAlive(): Boolean = {
+ if (process != null)
+ process.isAlive()
+ else
+ // return true on not-started process
+ true
+ }
+
+ def start_process(): Boolean = {
+ logger.debug("tlmgr process: entering starting process")
+ // process creation
+ if (process == null) {
+ logger.debug("tlmgr process: start_process: creating procIO")
+ val procIO = new ProcessIO(inputFn, outputFn(_, updout), outputFn(_, upderr))
+ val startCmd =
+ if (isWindows) {
+ logger.debug("detected Windows, using tlmgr.bat")
+ Seq("tlmgr.bat", "-v", "--machine-readable", "shell")
+ } else if (isCygwin) {
+ logger.debug("detected Cygwin, using bash -l -c tlmgr")
+ Seq("bash", "-l", "-c", "tlmgr -v --machine-readable shell")
+ } else {
+ logger.debug("detected Unixish, using tlmgr")
+ Seq("tlmgr", "-v", "--machine-readable", "shell")
+ }
+ val processBuilder: ProcessBuilder = startCmd
+ logger.debug("tlmgr process: start_process: running new tlmgr process")
+ process = processBuilder.run(procIO)
+ }
+ logger.debug("tlmgr process: start_process: checking for being alive")
+ process.isAlive()
+ }
+
+ def cleanup(): Unit = {
+ if (process != null) {
+ logger.debug("tlmgr process: cleanup - sending quit")
+ send_command("quit")
+ logger.debug("tlmgr process: cleanup - sleeping 2s")
+ Thread.sleep(1000)
+ logger.debug("tlmgr process: cleanup - destroying process")
+ process.destroy()
+ }
+ }
+
+ /* The standard input passing function */
+ private[this] def inputFn(stdin: OutputStream): Unit = {
+ val writer = new BufferedWriter(new OutputStreamWriter(stdin))
+ try {
+ var input = ""
+ while (true) {
+ input = inputString.take()
+ if (input == "quit") {
+ stdin.close()
+ return
+ } else {
+ writer.write(input + "\n")
+ logger.trace("writing " + input + " to process")
+ writer.flush()
+ }
+ }
+ stdin.close()
+ } catch {
+ case exc: Throwable =>
+ logger.warn("Exception in inputFn thread: " + exc + "\n")
+ stdin.close()
+ }
+ }
+
+ private[this] def outputFn(outStr: InputStream, updfun: String => Unit): Unit = {
+ val reader = new BufferedReader(new InputStreamReader(outStr))
+ try {
+ var line: String = ""
+ while (true) {
+ line = reader.readLine
+ logger.trace("DDD did read " + line + " from process")
+ try {
+ updfun(line)
+ } catch {
+ case exc: Throwable =>
+ logger.debug("Update output line function failed, continuing anyway. Exception: " + exc)
+ }
+ }
+ outStr.close()
+ } catch {
+ case exc: Throwable =>
+ logger.warn("Exception in outputFn thread: " + exc + "\n")
+ outStr.close()
+ }
+ }
+}
diff --git a/support/tlcockpit/src/main/scala/TeXLive/package.scala b/support/tlcockpit/src/main/scala/TeXLive/package.scala
new file mode 100644
index 0000000000..074a7a8615
--- /dev/null
+++ b/support/tlcockpit/src/main/scala/TeXLive/package.scala
@@ -0,0 +1,9 @@
+// TLCockpit
+// Copyright 2017-2018 Norbert Preining
+// Licensed according to GPLv3+
+//
+// Front end for tlmgr
+
+package object TeXLive {
+ val tlBlockSize = 4096 // TL gives sizes in multiples of BlockSize!
+}
diff --git a/support/tlcockpit/tlcockpit.man b/support/tlcockpit/tlcockpit.man
new file mode 100644
index 0000000000..b2b9febe65
--- /dev/null
+++ b/support/tlcockpit/tlcockpit.man
@@ -0,0 +1,32 @@
+.\" Hey, EMACS: -*- nroff -*-
+.TH TLCOCKPIT 1x "1 Nov 2018"
+.SH NAME
+tlcockpit \- GUI frontend for the TeX Live Manager tlmgr
+.SH SYNOPSIS
+.B tlcockpit
+.RI " [-d] [-dd] [-q] [-qq]"
+.SH DESCRIPTION
+TLCockpit aims at being a GUI for tlmgr, the
+TeX Live Manager, with a modern look and feel. We take inspiration
+from the TeX Live Utility.
+
+.SH OPTIONS
+.TP
+.B \-d
+be verbose
+.TP
+.B \-dd
+be even more verbose
+.TP
+.B \-q
+be quiet
+.TP
+.B \-qq
+be even more quiet
+
+.SH AUTHOR
+TLCockpit was written by Norbert Preining.
+
+.SH LICENSE
+TLCockpit is licensed under the GNU General Public License version 3+.
+