diff options
author | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
commit | e0c6872cf40896c7be36b11dcc744620f10adf1d (patch) | |
tree | 60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/tlcockpit |
Initial commit
Diffstat (limited to 'support/tlcockpit')
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 Binary files differnew file mode 100644 index 0000000000..8985309375 --- /dev/null +++ b/support/tlcockpit/logo/tlcockpit-banner.jpg diff --git a/support/tlcockpit/logo/tlcockpit-banner.xcf b/support/tlcockpit/logo/tlcockpit-banner.xcf Binary files differnew file mode 100644 index 0000000000..b4440043e7 --- /dev/null +++ b/support/tlcockpit/logo/tlcockpit-banner.xcf diff --git a/support/tlcockpit/logo/tlcockpit.xcf b/support/tlcockpit/logo/tlcockpit.xcf Binary files differnew file mode 100644 index 0000000000..ef7afaae26 --- /dev/null +++ b/support/tlcockpit/logo/tlcockpit.xcf 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 Binary files differnew file mode 100644 index 0000000000..b63c62214f --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-backups.jpg diff --git a/support/tlcockpit/screenshots/tlcockpit-collections.jpg b/support/tlcockpit/screenshots/tlcockpit-collections.jpg Binary files differnew file mode 100644 index 0000000000..d21be7098b --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-collections.jpg diff --git a/support/tlcockpit/screenshots/tlcockpit-config.jpg b/support/tlcockpit/screenshots/tlcockpit-config.jpg Binary files differnew file mode 100644 index 0000000000..efd6b1bf90 --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-config.jpg diff --git a/support/tlcockpit/screenshots/tlcockpit-loading.jpg b/support/tlcockpit/screenshots/tlcockpit-loading.jpg Binary files differnew file mode 100644 index 0000000000..9296cc582e --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-loading.jpg diff --git a/support/tlcockpit/screenshots/tlcockpit-menu-debug-expert.jpg b/support/tlcockpit/screenshots/tlcockpit-menu-debug-expert.jpg Binary files differnew file mode 100644 index 0000000000..0ec2ac43c7 --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-menu-debug-expert.jpg diff --git a/support/tlcockpit/screenshots/tlcockpit-packages.jpg b/support/tlcockpit/screenshots/tlcockpit-packages.jpg Binary files differnew file mode 100644 index 0000000000..5feb050a96 --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-packages.jpg diff --git a/support/tlcockpit/screenshots/tlcockpit-paper.jpg b/support/tlcockpit/screenshots/tlcockpit-paper.jpg Binary files differnew file mode 100644 index 0000000000..354c3d3560 --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-paper.jpg diff --git a/support/tlcockpit/screenshots/tlcockpit-pkgdetails.jpg b/support/tlcockpit/screenshots/tlcockpit-pkgdetails.jpg Binary files differnew file mode 100644 index 0000000000..01aa4b93dd --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-pkgdetails.jpg diff --git a/support/tlcockpit/screenshots/tlcockpit-postactions.jpg b/support/tlcockpit/screenshots/tlcockpit-postactions.jpg Binary files differnew file mode 100644 index 0000000000..4097948f9e --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-postactions.jpg diff --git a/support/tlcockpit/screenshots/tlcockpit-repositories.jpg b/support/tlcockpit/screenshots/tlcockpit-repositories.jpg Binary files differnew file mode 100644 index 0000000000..ae96799922 --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-repositories.jpg diff --git a/support/tlcockpit/screenshots/tlcockpit-updates.jpg b/support/tlcockpit/screenshots/tlcockpit-updates.jpg Binary files differnew file mode 100644 index 0000000000..9e7a8ef24f --- /dev/null +++ b/support/tlcockpit/screenshots/tlcockpit-updates.jpg 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 Binary files differnew file mode 100644 index 0000000000..10dfdccdbd --- /dev/null +++ b/support/tlcockpit/src/main/resources/TLCockpit/spinner-small.gif diff --git a/support/tlcockpit/src/main/resources/TLCockpit/spinner.gif b/support/tlcockpit/src/main/resources/TLCockpit/spinner.gif Binary files differnew file mode 100644 index 0000000000..bff38ea7a9 --- /dev/null +++ b/support/tlcockpit/src/main/resources/TLCockpit/spinner.gif diff --git a/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-128.jpg b/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-128.jpg Binary files differnew file mode 100644 index 0000000000..fe92ead1e3 --- /dev/null +++ b/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-128.jpg diff --git a/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-48.jpg b/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-48.jpg Binary files differnew file mode 100644 index 0000000000..d9d5dc6ca8 --- /dev/null +++ b/support/tlcockpit/src/main/resources/TLCockpit/tlcockpit-48.jpg 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+. + |