diff options
32 files changed, 1376 insertions, 692 deletions
diff --git a/Master/texmf-dist/doc/support/tlcockpit/NEWS b/Master/texmf-dist/doc/support/tlcockpit/NEWS index d8c84e675e8..1c12e2f46db 100644 --- a/Master/texmf-dist/doc/support/tlcockpit/NEWS +++ b/Master/texmf-dist/doc/support/tlcockpit/NEWS @@ -1,16 +1,29 @@ NEWS for tlcockpit -version 0.6 ------------ +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 ------------ +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 ------------ +version 0.4 (20171004) +---------------------- First half-way usable version diff --git a/Master/texmf-dist/doc/support/tlcockpit/README.md b/Master/texmf-dist/doc/support/tlcockpit/README.md index 4ee2115d88c..e151947c742 100644 --- a/Master/texmf-dist/doc/support/tlcockpit/README.md +++ b/Master/texmf-dist/doc/support/tlcockpit/README.md @@ -25,8 +25,8 @@ call as in `java -jar PATH/TO/tlcockpit.jar`. Requirements ------------ -You need at least `tlmgr` from after 2017-09-01, in TeX Live language at least -version 45190 or `texlive.infra`. +You need at least `tlmgr` from after 2017-11-23, in TeX Live language at least +version 45888 or `texlive.infra`. A recent Java installation that includes JavaFX is required. If you see errors like @@ -49,7 +49,7 @@ 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. -Deplyment is done via [sbt assembly](https://github.com/sbt/sbt-assembly) +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 @@ -58,24 +58,37 @@ writing Scala and my style is horrible ;-) Screenshots ----------- -(permanently outdated) +Starting up TLCockpit + +![Loading screen](screenshots/tlcockpit-loading.jpg) Package listing -![Package listing view](screenshots/pkgs-view.jpg) +![Package listing view](screenshots/tlcockpit-packages.jpg) + +View packages by collections + +![Packages by collections](screenshots/tlcockpit-collections.jpg) Updates listing -![Updates listing view](screenshots/update-view.jpg) +![Updates listing view](screenshots/tlcockpit-updates.jpg) Backup/Restore listing -![Backup listing view](screenshots/backup-view.jpg) +![Backup listing view](screenshots/tlcockpit-backups.jpg) Package details -![Package details](screenshots/pkg-details.png) +![Package details](screenshots/tlcockpit-pkgdetails.jpg) + +Configuration dialog + +![Config screen](screenshots/tlcockpit-config.jpg) + +Paper setting dialog +![Config screen](screenshots/tlcockpit-paper.jpg) License ------- diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/backup-view.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/backup-view.jpg Binary files differdeleted file mode 100644 index 552ec831b14..00000000000 --- a/Master/texmf-dist/doc/support/tlcockpit/screenshots/backup-view.jpg +++ /dev/null diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/pkg-details.png b/Master/texmf-dist/doc/support/tlcockpit/screenshots/pkg-details.png Binary files differdeleted file mode 100644 index 0b385b81169..00000000000 --- a/Master/texmf-dist/doc/support/tlcockpit/screenshots/pkg-details.png +++ /dev/null diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/pkgs-view.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/pkgs-view.jpg Binary files differdeleted file mode 100644 index ef0ca60ef4f..00000000000 --- a/Master/texmf-dist/doc/support/tlcockpit/screenshots/pkgs-view.jpg +++ /dev/null diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-backups.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-backups.jpg Binary files differnew file mode 100644 index 00000000000..20dbdf413a9 --- /dev/null +++ b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-backups.jpg diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-collections.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-collections.jpg Binary files differnew file mode 100644 index 00000000000..af5a3240ee4 --- /dev/null +++ b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-collections.jpg diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-config.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-config.jpg Binary files differnew file mode 100644 index 00000000000..efd6b1bf909 --- /dev/null +++ b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-config.jpg diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-loading.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-loading.jpg Binary files differnew file mode 100644 index 00000000000..40711b3d672 --- /dev/null +++ b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-loading.jpg diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-packages.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-packages.jpg Binary files differnew file mode 100644 index 00000000000..d31476c4887 --- /dev/null +++ b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-packages.jpg diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-paper.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-paper.jpg Binary files differnew file mode 100644 index 00000000000..354c3d3560d --- /dev/null +++ b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-paper.jpg diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-pkgdetails.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-pkgdetails.jpg Binary files differnew file mode 100644 index 00000000000..01aa4b93dd4 --- /dev/null +++ b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-pkgdetails.jpg diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-updates.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-updates.jpg Binary files differnew file mode 100644 index 00000000000..85052b11c63 --- /dev/null +++ b/Master/texmf-dist/doc/support/tlcockpit/screenshots/tlcockpit-updates.jpg diff --git a/Master/texmf-dist/doc/support/tlcockpit/screenshots/update-view.jpg b/Master/texmf-dist/doc/support/tlcockpit/screenshots/update-view.jpg Binary files differdeleted file mode 100644 index 1d10628df52..00000000000 --- a/Master/texmf-dist/doc/support/tlcockpit/screenshots/update-view.jpg +++ /dev/null diff --git a/Master/texmf-dist/scripts/tlcockpit/tlcockpit.jar b/Master/texmf-dist/scripts/tlcockpit/tlcockpit.jar Binary files differindex 0a57df3631f..ecfba0e9c1f 100644 --- a/Master/texmf-dist/scripts/tlcockpit/tlcockpit.jar +++ b/Master/texmf-dist/scripts/tlcockpit/tlcockpit.jar diff --git a/Master/texmf-dist/source/support/tlcockpit/.gitignore b/Master/texmf-dist/source/support/tlcockpit/.gitignore deleted file mode 100644 index d2d742875be..00000000000 --- a/Master/texmf-dist/source/support/tlcockpit/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.idea/ -target/ -project/target/ diff --git a/Master/texmf-dist/source/support/tlcockpit/build.sbt b/Master/texmf-dist/source/support/tlcockpit/build.sbt index fb5cf2f98d3..032d4016c04 100644 --- a/Master/texmf-dist/source/support/tlcockpit/build.sbt +++ b/Master/texmf-dist/source/support/tlcockpit/build.sbt @@ -1,13 +1,11 @@ name := "tlcockpit" -version := "0.6" +version := "0.7" scalaVersion := "2.12.3" libraryDependencies += "org.scalafx" % "scalafx_2.12" % "8.0.102-R11" - -// unmanagedJars in Compile += Attributed.blank(file(System.getenv("JAVA_HOME") + "/jre/lib/ext/jfxrt.jar")) -// unmanagedJars in Compile += Attributed.blank(file("/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/ext/jfxrt.jar")) +libraryDependencies += "io.spray" %% "spray-json" % "1.3.3" mainClass in assembly := Some("TLCockpit.ApplicationMain") diff --git a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/ApplicationMain.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/ApplicationMain.scala index 696744bbeb1..f37dc10806e 100644 --- a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/ApplicationMain.scala +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/ApplicationMain.scala @@ -6,22 +6,21 @@ package TLCockpit -import javafx.collections.ObservableList -import javafx.scene.control - -import TLCockpit.ApplicationMain.getClass +import TLCockpit.Utils._ import TeXLive._ -// import java.io.File -import scalafx.beans.property.StringProperty +import scala.collection.{immutable, mutable} import scala.collection.mutable.ArrayBuffer -import scala.concurrent.{Future, SyncVar} +import scala.concurrent.{Future, Promise, SyncVar} import scala.concurrent.ExecutionContext.Implicits.global -// import scala.reflect.io -// import scala.reflect.io.File import scala.util.{Failure, Success} -import scalafx.beans.property.ObjectProperty -import scalafx.geometry.{HPos, Pos, VPos, Orientation} +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} @@ -33,8 +32,6 @@ import scalafx.scene.control.TreeTableColumn._ import scalafx.scene.control.TreeItem import scalafx.scene.control.Menu._ import scalafx.scene.control.ListCell -import scalafx.scene.control.cell._ -import scalafx.scene.text.Text import scalafx.Includes._ import scalafx.application.{JFXApp, Platform} import scalafx.application.JFXApp.PrimaryStage @@ -43,21 +40,20 @@ import scalafx.scene.Scene import scalafx.scene.layout._ import scalafx.scene.control._ import scalafx.event.ActionEvent -import scala.sys.process._ import scalafx.collections.ObservableBuffer import scalafx.collections.ObservableMap -// import java.awt.Desktop +// JSON support - important load TLPackageJsonProtocol later! +import spray.json._ +import TeXLive.JsonProtocol._ -// TODO when installing a collection list the additionally installed packages, too -// TODO idea pkg tree with collection -> packages -// TODO pkg info listview adjust height according to font size and # of items -// TODO TreeTableView indentation is lazy object ApplicationMain extends JFXApp { val version: String = getClass.getPackage.getImplementationVersion + var tlmgrBusy = BooleanProperty(false) + // necessary action when Window is closed with X or some other operation override def stopApp(): Unit = { tlmgr.cleanup() @@ -66,26 +62,41 @@ object ApplicationMain extends JFXApp { val iconImage = new Image(getClass.getResourceAsStream("tlcockpit-48.jpg")) val logoImage = new Image(getClass.getResourceAsStream("tlcockpit-128.jpg")) - val pkgs: ObservableMap[String, TLPackage] = ObservableMap[String, TLPackage]() - val upds: ObservableMap[String, TLUpdate] = ObservableMap[String, TLUpdate]() - val bkps: ObservableMap[String, Map[String, TLBackup]] = ObservableMap[String, Map[String,TLBackup]]() // pkgname -> (version -> TLBackup)* + val tlpkgs: ObservableMap[String, TLPackage] = ObservableMap[String,TLPackage]() + 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 errorText: ObservableBuffer[String] = ObservableBuffer[String]() + 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") @@ -93,7 +104,11 @@ object ApplicationMain extends JFXApp { }) val update_all_menu: MenuItem = new MenuItem("Update all") { - onAction = (ae) => callback_update("--all") + val cmd = "--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") { @@ -110,7 +125,12 @@ object ApplicationMain extends JFXApp { content = outputfield }, new Tab { - text = "Error" + text = "Logging" + closable = false + content = logfield + }, + new Tab { + text = "Errors" closable = false content = errorfield } @@ -128,26 +148,8 @@ object ApplicationMain extends JFXApp { (ae: KeyEvent) => if (ae.code == KeyCode.Enter) callback_run_cmdline() } val outputLine = new SyncVar[String] + val errorLine = new SyncVar[String] - def tlmgr_async_command(s: String, f: Array[String] => Unit): Unit = { - errorText.clear() - outputText.clear() - outerrpane.expanded = false - statusMenu.text = "Status: Busy" - val foo = Future { - tlmgr.send_command(s) - } - foo onComplete { - case Success(ret) => - f(ret) - Platform.runLater { statusMenu.text = "Status: Idle" } - case Failure(t) => - errorText.append("An ERROR has occurred calling tlmgr: " + t.getMessage) - outerrpane.expanded = true - outerrtabs.selectionModel().select(1) - Platform.runLater { statusMenu.text = "Status: Idle" } - } - } def callback_quit(): Unit = { tlmgr.cleanup() @@ -156,12 +158,12 @@ object ApplicationMain extends JFXApp { } def callback_run_text(s: String): Unit = { - tlmgr_async_command(s, (s: Array[String]) => {}) + tlmgr_send(s, (a: String, b: Array[String]) => {}) } def callback_run_cmdline(): Unit = { - tlmgr_async_command(cmdline.text.value, s => { - outputText.appendAll(s) + tlmgr_send(cmdline.text.value, (status,output) => { + outputText.append(output.mkString("\n")) outerrpane.expanded = true outerrtabs.selectionModel().select(0) }) @@ -199,7 +201,7 @@ object ApplicationMain extends JFXApp { def callback_run_external(s: String, unbuffered: Boolean = true): Unit = { outputText.clear() - errorText.clear() + // logText.clear() outerrpane.expanded = true outerrtabs.selectionModel().select(0) outputText.append(s"Running $s" + (if (unbuffered) " (unbuffered)" else " (buffered)")) @@ -207,7 +209,7 @@ object ApplicationMain extends JFXApp { s ! ProcessLogger( line => if (unbuffered) Platform.runLater( outputText.append(line) ) else OutputBuffer.synchronized( OutputBuffer.append(line + "\n") ), - line => Platform.runLater( errorText.append(line) ) + line => Platform.runLater( logText.append(line) ) ) } foo.onComplete { @@ -227,7 +229,7 @@ object ApplicationMain extends JFXApp { errorText.append("An ERROR has occurred running $s: " + t.getMessage) errorfield.scrollTop = Double.MaxValue outerrpane.expanded = true - outerrtabs.selectionModel().select(1) + outerrtabs.selectionModel().select(2) } } } @@ -242,11 +244,10 @@ object ApplicationMain extends JFXApp { }.showAndWait() } - def callback_update(s: String): Unit = { - var prevUpdName = "" - var prevUpdPkg = new TLUpdate("","","","","","") - lineUpdateFunc = (l:String) => { - // println("line update: " + l + "=") + def set_line_update_function(mode: String) = { + var prevName = "" + stdoutLineUpdateFunc = (l:String) => { + // println("DEBUG line update: " + l + "=") l match { case u if u.startsWith("location-url") => None case u if u.startsWith("total-bytes") => None @@ -255,84 +256,115 @@ object ApplicationMain extends JFXApp { case u if u == "OK" => None case u if u.startsWith("tlmgr>") => None case u => - if (prevUpdName != "") { - // println("Removing " + prevUpdName + " from list!") - upds.remove(prevUpdName) - val op = pkgs(prevUpdName) - pkgs(prevUpdName) = new TLPackage(prevUpdPkg.name.value, op.rrev.value.toString, op.rrev.value.toString, - prevUpdPkg.shortdesc.value, op.size.value.toString, "Installed") - Platform.runLater { + 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") - trigger_update("pkgs") + } 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")) { // nothing to be done, all has been done above - // println("got end of updates") + // println("DEBUG got end of updates") } else { - // println("getting update line") - val foo = parse_one_update_line(l) - val pkgname = foo.name.value - val PkgTreeItemOption = updateTable.root.value.children.find(_.value.value.name.value == pkgname) - PkgTreeItemOption match { - case Some(p) => - prevUpdName = pkgname - prevUpdPkg = p.getValue - // println("Setting status to Updating ... for " + pkgname) - prevUpdPkg.status = StringProperty("Updating ...") - Platform.runLater { - // println("calling updateTable refresh") - updateTable.refresh() - } - case None => println("Very strange: Cannot find TreeItem for upd package" + pkgname) + // println("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 } } } } - // val cmd = if (s == "--self") "update --self --no-restart" else s"update $s" + } + + def callback_update(s: String): Unit = { + set_line_update_function("update") val cmd = if (s == "--self") "update --self" else s"update $s" - tlmgr_async_command(cmd, _ => { - Platform.runLater { - lineUpdateFunc = { (s: String) => } - if (s == "--self") { - reinitialize_tlmgr() - // this doesn't work seemingly - // update_upds_list() - } + tlmgr_send(cmd, (a,b) => { + stdoutLineUpdateFunc = defaultStdoutLineUpdateFunc + if (s == "--self") { + reinitialize_tlmgr() + // this doesn't work seemingly + // update_upds_list() } }) } - - - def do_one_pkg(what: String, pkg: String): Unit = { - tlmgr_async_command(s"$what $pkg", _ => { Platform.runLater { update_pkgs_lists() } }) + 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_pkg(str: String, rev: String): Unit = { - not_implemented_info() + 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() + }) } bkps.onChange( (obs,chs) => { - var doit = chs match { + 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) { // println("DEBUG bkps.onChange called new length = " + bkps.keys.toArray.length) - val newroot = new TreeItem[TLBackup](new TLBackup("root", "", "")) { + 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, TLBackup)] = p._2.toArray.sortBy(-_._2.rev.value.toInt) + val versmap: Array[(String, TLBackupDisplay)] = p._2.toArray.sortBy(-_._2.rev.value.toInt) - val foo: Seq[TreeItem[TLBackup]] = versmap.tail.sortBy(-_._2.rev.value.toInt).map { q => - new TreeItem[TLBackup](q._2) + val foo: Seq[TreeItem[TLBackupDisplay]] = versmap.tail.sortBy(-_._2.rev.value.toInt).map { q => + new TreeItem[TLBackupDisplay](q._2) }.toSeq - new TreeItem[TLBackup](versmap.head._2) { + new TreeItem[TLBackupDisplay](versmap.head._2) { children = foo } }).toArray.sortBy(_.value.value.name.value) @@ -342,63 +374,166 @@ object ApplicationMain extends JFXApp { } } }) + + 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) => { - var doit = chs match { + 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" + // don't call the trigger on root removal! + // case ObservableMap.Remove(k, v) => k.toString == "root" + case ObservableMap.Remove(k,v) => false } if (doit) { - val pkgbuf: ArrayBuffer[TLPackage] = ArrayBuffer.empty[TLPackage] - val binbuf = scala.collection.mutable.Map.empty[String, ArrayBuffer[TLPackage]] + // println("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.stripPrefix("texlive.").contains(".")) { - val foo: Array[String] = pkg._1.stripPrefix("texlive.infra").split('.') + 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) - val binname = foo(1) - if (binbuf.keySet.contains(pkgname)) { - binbuf(pkgname) += pkg._2 - } else { - binbuf(pkgname) = ArrayBuffer[TLPackage](pkg._2) + 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._2 + pkgbuf(pkg._1) = pkg._2 } }) - // now we have all normal packages in pkgbuf, and its sub-packages in binbuf - // we need to create TreeItems - val viewkids: ArrayBuffer[TreeItem[TLPackage]] = pkgbuf.map { p => { - val kids: Seq[TLPackage] = if (binbuf.keySet.contains(p.name.value)) { - binbuf(p.name.value) - } else { - Seq() + // 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 => println("-> " + f.name.value)) + } else { + // println("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 + } } - // for ismixed we && all the installed status. If all are installed, we get true - val allinstalled = (kids :+ p).foldRight[Boolean](true)((k, b) => k.installed.value == "Installed" && b) - val someinstalled = (kids :+ p).exists(_.installed.value == "Installed") - val mixedinstalled = !allinstalled && someinstalled - if (mixedinstalled) { - // replace installed status with "Mixed" - new TreeItem[TLPackage](new TreeItem[TLPackage]( - new TLPackage(p.name.value, p.lrev.value.toString, p.rrev.value.toString, p.shortdesc.value, p.size.value.toString, "Mixed") - )) { - children = kids.map(new TreeItem[TLPackage](_)) + }) + // 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 { - new TreeItem[TLPackage](p) { - children = kids.map(new TreeItem[TLPackage](_)) - } + 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) + // println("DEBUG: leaving pkgs.onChange before runLater") Platform.runLater { - packageTable.root = new TreeItem[TLPackage](new TLPackage("root", "0", "0", "", "0", "")) { + packageTable.root = new TreeItem[TLPackageDisplay](new TLPackageDisplay("root", "0", "0", "", "0", "")) { expanded = true children = viewkids.sortBy(_.value.value.name.value) } @@ -415,10 +550,10 @@ object ApplicationMain extends JFXApp { 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[TLUpdate](new TLUpdate("root", "", "", "", "", "")) { + val newroot = new TreeItem[TLUpdateDisplay](new TLUpdateDisplay("root", "", "", "", "", "")) { children = upds .filter(_._1 != "root") - .map(p => new TreeItem[TLUpdate](p._2)) + .map(p => new TreeItem[TLUpdateDisplay](p._2)) .toArray .sortBy(_.value.value.name.value) } @@ -442,37 +577,65 @@ object ApplicationMain extends JFXApp { }.showAndWait() } - def update_bkps_list(): Unit = { - tlmgr_async_command("restore", lines => { - // lines.drop(1).foreach(println(_)) - val newbkps: Map[String, Map[String, TLBackup]] = lines.drop(1).map { (l: String) => - val fields = l.split("[ ():]", -1).filter(_.nonEmpty) - val pkgname = fields(0) - val rests: Array[Array[String]] = fields.drop(1).sliding(4, 4).toArray - (pkgname, rests.map({ p => (p(1), new TLBackup(pkgname, p(0), p(1) + " " + p(2) + ":" + p(3)))}).toMap) - }.toMap + def load_backups_update_bkps_view(): Unit = { + val tmp = new Label("Loading backups, please wait ...") + tmp.wrapText = true + tmp.opacity = 0.4f + tmp.font = new Font(30f) + val prevph = backupTable.placeholder.value + backupTable.placeholder = tmp + 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 ++= newbkps + bkps ++= backups trigger_update("bkps") + backupTable.placeholder = prevph }) } - def update_pkgs_lists():Unit = { - tlmgr_async_command("info --data name,localrev,remoterev,shortdesc,size,installed", (s: Array[String]) => { - val newpkgs = s.map { (line: String) => - val fields: Array[String] = line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1) - val sd = fields(3) - val shortdesc = if (sd.isEmpty) "" else sd.substring(1).dropRight(1).replace("""\"""",""""""") - val inst = if (fields(5) == "1") "Installed" else "Not installed" - (fields(0), new TLPackage(fields(0), fields(1), fields(2), shortdesc, fields(4), inst)) - }.toMap - pkgs.clear() - pkgs ++= newpkgs - trigger_update("pkgs") - }) + + 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 tmp = new Label("Loading database, please wait ...") + tmp.wrapText = true + tmp.opacity = 0.4f + tmp.font = new Font(30f) + val prevph = packageTable.placeholder.value + packageTable.placeholder = tmp + tlmgr_send("info --json", (status, lines) => { + val jsonAst = lines.mkString("").parseJson + tlpkgs.clear() + tlpkgs ++= jsonAst.convertTo[List[TLPackage]].map { p => (p.name, p)} + update_pkgs_view() + packageTable.placeholder = prevph + }) + } - def parse_one_update_line(l: String): TLUpdate = { + def parse_one_update_line(l: String): TLUpdateDisplay = { val fields = l.split("\t") val pkgname = fields(0) val status = fields(1) match { @@ -486,15 +649,15 @@ object ApplicationMain extends JFXApp { } val localrev = fields(2) val serverrev = fields(3) - val size = humanReadableByteSize(fields(4).toLong) + 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: TLPackage = pkgs(pkgname) + val tlpkg: TLPackageDisplay = pkgs(pkgname) val shortdesc = tlpkg.shortdesc.value - new TLUpdate(pkgname, status, + new TLUpdateDisplay(pkgname, status, localrev + { if (lctanv != "-") s" ($lctanv)" else "" }, @@ -504,10 +667,17 @@ object ApplicationMain extends JFXApp { shortdesc, size) } - def update_upds_list(): Unit = { - tlmgr_async_command("update --list", lines => { - // val newupds = scala.collection.mutable.Map.empty[String,TLUpdate] - val newupds: Map[String, TLUpdate] = lines.filter { l => + def load_updates_update_upds_view(): Unit = { + val tmp = new Label("Loading updates, please wait ...") + tmp.wrapText = true + tmp.opacity = 0.4f + tmp.font = new Font(30f) + val prevph = updateTable.placeholder.value + updateTable.placeholder = tmp + tlmgr_send("update --list", (status, lines) => { + // println(s"DEBUG got updates length ${lines.length}") + // println(s"DEBUG tlmgr last output = ${lines}") + val newupds: Map[String, TLUpdateDisplay] = lines.filter { l => l match { case u if u.startsWith("location-url") => false case u if u.startsWith("total-bytes") => false @@ -527,17 +697,19 @@ object ApplicationMain extends JFXApp { upds ++= newupds } trigger_update("upds") + updateTable.placeholder = prevph }) } def trigger_update(s:String): Unit = { - // println("Triggering update of " + s) - if (s == "pkgs") - pkgs("root") = new TLPackage("root","0","0","","0","") - else if (s == "upds") - upds("root") = new TLUpdate("root", "", "", "", "", "") - else if (s == "bkps") - bkps("root") = Map[String,TLBackup](("0", new TLBackup("root","0","0"))) + // println("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 = { @@ -560,7 +732,6 @@ object ApplicationMain extends JFXApp { vb.prefHeight = 150 vb.maxHeight = 200 vb.vgrow = Priority.Always - // TODO tighter spacing for ListView vb.orientation = Orientation.Vertical vb.cellFactory = { p => { val foo = new ListCell[String] @@ -582,210 +753,18 @@ object ApplicationMain extends JFXApp { } } - def callback_show_pkg_info(pkg: String): Unit = { - tlmgr_async_command(s"info --list $pkg", pkginfo => { - // need to call runLater to go back to main thread! - Platform.runLater { - 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 = pkgs(pkg).installed.value == "Installed" - val grid = new GridPane() { - hgap = 10 - vgap = 10 - padding = Insets(20) - } - var crow = 0 - val runFiles = ArrayBuffer[String]() - val docFiles = ArrayBuffer[String]() - val binFiles = ArrayBuffer[String]() - val srcFiles = ArrayBuffer[String]() - var whichFiles = "run" // we assume run files if not found! - pkginfo.foreach((line: String) => { - if (line.startsWith(" ")) { - // files are listed with two spaces in the beginning - whichFiles match { - case "run" => runFiles += line.trim - case "doc" => docFiles += line.trim - case "bin" => binFiles += line.trim - case "src" => srcFiles += line.trim - case _ => println("Don't know what to do?!?!") - } - } else { - val keyval = line.split(":", 2).map(_.trim) - whichFiles = if (keyval(0).startsWith("run files")) "run" else - if (keyval(0).startsWith("doc files")) "doc" else - if (keyval(0).startsWith("source files")) "src" else - if (keyval(0).startsWith("bin files (all platforms)")) "bin" else - "unknown" - if (keyval.length == 2) { - if (keyval(0) == "doc files" || keyval(0) == "source files" || keyval(0) == "run files" || - keyval(0).startsWith("depending package ") || - keyval(0).startsWith("bin files (all platforms)") || keyval(0).startsWith("Included files, by type")) { - // do nothing - } else { - val keylabel = new Label(keyval(0)) - val vallabel = new Label(keyval(1)) - vallabel.wrapText = true - grid.add(keylabel, 0, crow) - grid.add(vallabel, 1, crow) - crow += 1 - } - } - } - }) - // add files section - if (docFiles.nonEmpty) { - grid.add(new Label("doc files"), 0, crow) - //grid.add(new Label(docFiles.mkString("\n")) { wrapText = true },1, crow) - grid.add(doListView(docFiles.map(s => s.replaceFirst("RELOC","texmf-dist")),isInstalled),1,crow) - crow += 1 - } - if (runFiles.nonEmpty) { - grid.add(new Label("run files"), 0, crow) - // grid.add(new Label(runFiles.mkString("\n")) { wrapText = true },1, crow) - grid.add(doListView(runFiles.map(s => s.replaceFirst("RELOC","texmf-dist")),false),1,crow) - crow += 1 - } - if (srcFiles.nonEmpty) { - grid.add(new Label("src files"), 0, crow) - // grid.add(new Label(srcFiles.mkString("\n")) { wrapText = true },1, crow) - grid.add(doListView(srcFiles.map(s => s.replaceFirst("RELOC","texmf-dist")),false),1,crow) - crow += 1 - } - if (binFiles.nonEmpty) { - grid.add(new Label("bin files"), 0, crow) - // grid.add(new Label(binFiles.mkString("\n")) { wrapText = true },1, crow) - grid.add(doListView(binFiles.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, new HPos(HPos.Left), true)) - dialog.dialogPane().content = grid - dialog.width = 600 - dialog.height = 1500 - dialog.showAndWait() - } - }) - } - def callback_show_pkg_info_dev(pkg: String): Unit = { - tlmgr_async_command(s"info --list $pkg", pkginfo => { - // need to call runLater to go back to main thread! - Platform.runLater { - 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 keyCol = new TreeTableColumn[(String,String), String] { - text = "Package" - cellValueFactory = { p => StringProperty(p.value.value.value._1) } - } - val valCol = new TreeTableColumn[(String,String), String] { - text = pkg - cellValueFactory = { p => StringProperty(p.value.value.value._2) } - } - val infoTable = new TreeTableView[(String, String)]( - new TreeItem[(String, String)](("","")) { - expanded = false - }) { - columns ++= List(keyCol, valCol) - } - valCol.prefWidth.bind(infoTable.width - keyCol.width - 15) - infoTable.prefHeight = 300 - infoTable.vgrow = Priority.Always - infoTable.showRoot = false - infoTable.fixedCellSize = Region.USE_COMPUTED_SIZE - val runFiles = ArrayBuffer[String]() - val docFiles = ArrayBuffer[String]() - val binFiles = ArrayBuffer[String]() - val srcFiles = ArrayBuffer[String]() - val kids = ArrayBuffer[TreeItem[(String,String)]]() - var whichFiles = "run" // we assume run files if not found! - pkginfo.foreach((line: String) => { - if (line.startsWith(" ")) { - // files are listed with two spaces in the beginning - whichFiles match { - case "run" => runFiles += line.trim - case "doc" => docFiles += line.trim - case "bin" => binFiles += line.trim - case "src" => srcFiles += line.trim - case _ => println("Don't know what to do?!?!") - } - } else { - val keyval = line.split(":", 2).map(_.trim) - whichFiles = if (keyval(0).startsWith("run files")) "run" else - if (keyval(0).startsWith("doc files")) "doc" else - if (keyval(0).startsWith("source files")) "src" else - if (keyval(0).startsWith("bin files (all platforms)")) "bin" else - "unknown" - if (keyval.length == 2) { - if (keyval(0) == "doc files" || keyval(0) == "source files" || keyval(0) == "run files" || - keyval(0).startsWith("bin files (all platforms)")) { - // do nothing - } else { - kids += new TreeItem[(String,String)]((keyval(0), keyval(1))) - } - } - } - }) - // add files section - if (docFiles.nonEmpty) { - kids += new TreeItem[(String,String)](("Doc files","")) { - children = docFiles.map { p => new TreeItem[(String,String)](("",p)) } - } - } - if (runFiles.nonEmpty) { - kids += new TreeItem[(String,String)](("Run files","")) { - children = runFiles.map { p => new TreeItem[(String,String)](("",p)) } - } - } - if (srcFiles.nonEmpty) { - kids += new TreeItem[(String,String)](("Source files","")) { - children = srcFiles.map { p => new TreeItem[(String,String)](("",p)) } - } - } - if (binFiles.nonEmpty) { - kids += new TreeItem[(String,String)](("Bin files","")) { - children = binFiles.map { p => new TreeItem[(String,String)](("",p)) } - } - } - infoTable.root = new TreeItem[(String, String)](("","")) { - expanded = true - children = kids - } - dialog.dialogPane().content = infoTable - // dialog.width = 500 - // dialog.height = 800 - dialog.showAndWait() - } - }) - } - - /** - * @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)}" - } - val mainMenu: Menu = new Menu("TLCockpit") { items = List( - update_all_menu, - update_self_menu, - new SeparatorMenuItem, + // 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("mktexlsr") @@ -797,49 +776,96 @@ object ApplicationMain extends JFXApp { new MenuItem("Rebuild all formats ...") { onAction = (ae) => callback_run_external("fmtutil --sys --all", false) }, new MenuItem("Update font map database ...") { onAction = (ae) => callback_run_external("updmap --sys") - }, - /* - new MenuItem("Restore packages from backup ...") { - disable = true; onAction = (ae) => not_implemented_info() - }, - new MenuItem("Handle symlinks in system dirs ...") { - disable = true; onAction = (ae) => not_implemented_info() - }, - new SeparatorMenuItem, - new MenuItem("Remove TeX Live ...") { - disable = true; onAction = (ae) => not_implemented_info() - }, - */ + } + ) + } + 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, - // temporarily move here as we disable the Help menu - new MenuItem("About") { - onAction = (ae) => callback_about() - }, - new MenuItem("Exit") { - onAction = (ae: ActionEvent) => callback_quit() - }) + 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) => + // println(s"Got result ${newPapers}") + // collect changed settings + val changedPapers = newPapers.filter(p => currentPapers(p._1) != p._2) + // println(s"Got changed papers ${changedPapers}") + changedPapers.foreach(p => { + tlmgr_send(s"paper ${p._1} paper ${p._2}", (_,_) => None) + }) + case None => + } + } + }) + } + val optionsMenu: Menu = new Menu("Options") { - items = List( new MenuItem("General ...") { disable = true; onAction = (ae) => not_implemented_info() }, - new MenuItem("Paper ...") { disable = true; onAction = (ae) => not_implemented_info() }, - new MenuItem("Platforms ...") { disable = true; onAction = (ae) => not_implemented_info() }, + 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 helpMenu: Menu = new Menu("Help") { - items = List( - /* - new MenuItem("Manual") { - disable = true; onAction = (ae) => not_implemented_info() - }, - */ - new MenuItem("About") { - onAction = (ae) => callback_about() - }, + new CheckMenuItem("Disable auto-removal of server-deleted packages") { disable = true } */ ) } val statusMenu: Menu = new Menu("Status: Idle") { @@ -867,39 +893,39 @@ object ApplicationMain extends JFXApp { ) } } - val updateTable: TreeTableView[TLUpdate] = { - val colName = new TreeTableColumn[TLUpdate, String] { + val updateTable: TreeTableView[TLUpdateDisplay] = { + val colName = new TreeTableColumn[TLUpdateDisplay, String] { text = "Package" cellValueFactory = { _.value.value.value.name } prefWidth = 150 } - val colStatus = new TreeTableColumn[TLUpdate, String] { + val colStatus = new TreeTableColumn[TLUpdateDisplay, String] { text = "Status" cellValueFactory = { _.value.value.value.status } prefWidth = 120 } - val colDesc = new TreeTableColumn[TLUpdate, String] { + val colDesc = new TreeTableColumn[TLUpdateDisplay, String] { text = "Description" cellValueFactory = { _.value.value.value.shortdesc } prefWidth = 300 } - val colLRev = new TreeTableColumn[TLUpdate, String] { + val colLRev = new TreeTableColumn[TLUpdateDisplay, String] { text = "Local rev" cellValueFactory = { _.value.value.value.lrev } prefWidth = 100 } - val colRRev = new TreeTableColumn[TLUpdate, String] { + val colRRev = new TreeTableColumn[TLUpdateDisplay, String] { text = "Remote rev" cellValueFactory = { _.value.value.value.rrev } prefWidth = 100 } - val colSize = new TreeTableColumn[TLUpdate, String] { + val colSize = new TreeTableColumn[TLUpdateDisplay, String] { text = "Size" cellValueFactory = { _.value.value.value.size } prefWidth = 70 } - val table = new TreeTableView[TLUpdate]( - new TreeItem[TLUpdate](new TLUpdate("root","","","","","")) { + val table = new TreeTableView[TLUpdateDisplay]( + new TreeItem[TLUpdateDisplay](new TLUpdateDisplay("root","","","","","")) { expanded = false }) { columns ++= List(colName, colStatus, colDesc, colLRev, colRRev, colSize) @@ -910,47 +936,60 @@ object ApplicationMain extends JFXApp { table.placeholder = new Label("No updates available") table.showRoot = false table.rowFactory = { _ => - val row = new TreeTableRow[TLUpdate] {} - val ctm = new ContextMenu( - new MenuItem("Info") { - onAction = (ae) => callback_show_pkg_info(row.item.value.name.value) - }, - new MenuItem("Install") { - // onAction = (ae) => callback_run_text("install " + row.item.value.name.value) - onAction = (ae) => do_one_pkg("install", row.item.value.name.value) - }, - new MenuItem("Remove") { - // onAction = (ae) => callback_run_text("remove " + row.item.value.name.value) - onAction = (ae) => do_one_pkg("remove", row.item.value.name.value) - }, - new MenuItem("Update") { - // onAction = (ae) => callback_run_text("update " + row.item.value.name.value) - onAction = (ae) => callback_update(row.item.value.name.value) + val row = new TreeTableRow[TLUpdateDisplay] {} + val infoMI = new MenuItem("Info") { + onAction = (ae) => new PkgInfoDialog(row.item.value.name.value).showAndWait() + } + 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[TLPackage] = { - val colName = new TreeTableColumn[TLPackage, String] { + val packageTable: TreeTableView[TLPackageDisplay] = { + val colName = new TreeTableColumn[TLPackageDisplay, String] { text = "Package" cellValueFactory = { _.value.value.value.name } prefWidth = 150 } - val colDesc = new TreeTableColumn[TLPackage, String] { + val colDesc = new TreeTableColumn[TLPackageDisplay, String] { text = "Description" cellValueFactory = { _.value.value.value.shortdesc } prefWidth = 300 } - val colInst = new TreeTableColumn[TLPackage, String] { + val colInst = new TreeTableColumn[TLPackageDisplay, String] { text = "Installed" cellValueFactory = { _.value.value.value.installed } prefWidth = 100 } - val table = new TreeTableView[TLPackage]( - new TreeItem[TLPackage](new TLPackage("root","0","0","","0","")) { + val table = new TreeTableView[TLPackageDisplay]( + new TreeItem[TLPackageDisplay](new TLPackageDisplay("root","0","0","","0","")) { expanded = false }) { columns ++= List(colName, colDesc, colInst) @@ -959,42 +998,48 @@ object ApplicationMain extends JFXApp { table.prefHeight = 300 table.showRoot = false table.vgrow = Priority.Always - table.rowFactory = { _ => - val row = new TreeTableRow[TLPackage] {} - val ctm = new ContextMenu( - new MenuItem("Info") { - onAction = (ae) => callback_show_pkg_info(row.item.value.name.value) - }, - new MenuItem("Install") { - onAction = (ae) => do_one_pkg("install", row.item.value.name.value) - }, - new MenuItem("Remove") { - onAction = (ae) => do_one_pkg("remove", row.item.value.name.value) + table.rowFactory = { p => + val row = new TreeTableRow[TLPackageDisplay] {} + val infoMI = new MenuItem("Info") { + onAction = (ae) => new PkgInfoDialog(row.item.value.name.value).showAndWait() + } + 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[TLBackup] = { - val colName = new TreeTableColumn[TLBackup, String] { + val backupTable: TreeTableView[TLBackupDisplay] = { + val colName = new TreeTableColumn[TLBackupDisplay, String] { text = "Package" cellValueFactory = { _.value.value.value.name } prefWidth = 150 } - val colRev = new TreeTableColumn[TLBackup, String] { + val colRev = new TreeTableColumn[TLBackupDisplay, String] { text = "Revision" cellValueFactory = { _.value.value.value.rev } prefWidth = 100 } - val colDate = new TreeTableColumn[TLBackup, String] { + val colDate = new TreeTableColumn[TLBackupDisplay, String] { text = "Date" cellValueFactory = { _.value.value.value.date } prefWidth = 300 } - val table = new TreeTableView[TLBackup]( - new TreeItem[TLBackup](new TLBackup("root","","")) { + val table = new TreeTableView[TLBackupDisplay]( + new TreeItem[TLBackupDisplay](new TLBackupDisplay("root","","")) { expanded = false }) { columns ++= List(colName, colRev, colDate) @@ -1004,13 +1049,13 @@ object ApplicationMain extends JFXApp { table.showRoot = false table.vgrow = Priority.Always table.rowFactory = { _ => - val row = new TreeTableRow[TLBackup] {} + val row = new TreeTableRow[TLBackupDisplay] {} val ctm = new ContextMenu( new MenuItem("Info") { - onAction = (ae) => callback_show_pkg_info(row.item.value.name.value) + onAction = (ae) => new PkgInfoDialog(row.item.value.name.value).showAndWait() }, new MenuItem("Restore") { - onAction = (ae) => callback_restore_pkg(row.item.value.name.value, row.item.value.rev.value) + onAction = (ae) => callback_restore(row.item.value.name.value, row.item.value.rev.value) } ) row.contextMenu = ctm @@ -1018,6 +1063,36 @@ object ApplicationMain extends JFXApp { } table } + val searchEntry = new TextField() + searchEntry.hgrow = Priority.Sometimes + searchEntry.onKeyPressed = { + (ae: KeyEvent) => if (ae.code == KeyCode.Enter) update_pkgs_view() + } + 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 + padding = Insets(10) + spacing = 10 + } + val pkgsContainer = new VBox { + children = Seq(packageTable,searchBox) + } + val pkgstabs: TabPane = new TabPane { minWidth = 400 vgrow = Priority.Always @@ -1025,7 +1100,7 @@ object ApplicationMain extends JFXApp { new Tab { text = "Packages" closable = false - content = packageTable + content = pkgsContainer }, new Tab { text = "Updates" @@ -1039,23 +1114,31 @@ object ApplicationMain extends JFXApp { } ) } + // 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, statusMenu) + } pkgstabs.selectionModel().selectedItem.onChange( (a,b,c) => { if (a.value.text() == "Backups") { if (backupTable.root.value.children.length == 0) - update_bkps_list() + load_backups_update_bkps_view() + menuBar.menus = Seq(mainMenu, toolsMenu, optionsMenu, statusMenu) } else if (a.value.text() == "Updates") { // only update if not done already if (updateTable.root.value.children.length == 0) - update_upds_list() + load_updates_update_upds_view() + menuBar.menus = Seq(mainMenu, updMenu, toolsMenu, optionsMenu, statusMenu) + } else if (a.value.text() == "Packages") { + menuBar.menus = Seq(mainMenu, pkgsMenu, toolsMenu, optionsMenu, statusMenu) } } ) - val menuBar: MenuBar = new MenuBar { - useSystemMenuBar = true - // menus.addAll(mainMenu, optionsMenu, helpMenu) - menus.addAll(mainMenu, statusMenu) - } + stage = new PrimaryStage { title = "TLCockpit" @@ -1083,42 +1166,131 @@ object ApplicationMain extends JFXApp { 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 val tt = new TlmgrProcess( - // (s:String) => outputfield.text = s, - (s: Array[String]) => { - // we don't wont normal output to be displayed - // as it is anyway returned - // outputText.clear() - // outputText.appendAll(s) - }, - (s: String) => { - // errorText.clear() - errorText.append(s) - if (s.trim != "") { - outerrpane.expanded = true - outerrtabs.selectionModel().select(1) - } - }, - (s: String) => outputLine.put(s) + (s: String) => outputLine.put(s), + (s: String) => errorLine.put(s) ) - val bar = Future { + /* val tlmgrMonitor = Future { while (true) { + if (!tlmgr.isAlive) { + println("TLMGR HAS DIED!!!!") + // Platform.exit() + // sys.exit(1) + } + Thread.sleep(5000) + } + }*/ + val stdoutFuture = Future { + val tlmgrOutput = ArrayBuffer[String]() + var tlmgrStatus = "" + var alive = true + while (alive) { val s = outputLine.take - lineUpdateFunc(s) + if (s == null) { + alive = false + // println("GOT NULL from outputline tlmgr dead???") + } else { + //println(s"DEBUG: got " + s) + if (s == "OK") { + tlmgrStatus = s + } else if (s == "ERROR") { + tlmgrStatus = s + } else if (s == "tlmgr> ") { + // println("DEBUG: fulfilling current promise!") + currentPromise.success((tlmgrStatus, tlmgrOutput.toArray)) + tlmgrStatus = "" + tlmgrOutput.clear() + tlmgrBusy.value = false + if (pendingJobs.nonEmpty) { + val nextCmd = pendingJobs.dequeue() + tlmgr_run_one_cmd(nextCmd._1, nextCmd._2) + } + } else { + tlmgrOutput += s + stdoutLineUpdateFunc(s) + } + } + } + } + tlmgrBusy.onChange({ Platform.runLater{ statusMenu.text = "Status: " + (if (tlmgrBusy.value) "Busy" else "Idle") }}) + stdoutFuture.onComplete { + case Success(value) => + // println(s"tlmgr stdout reader terminated: ${value}") + Platform.runLater { + outerrpane.expanded = true + outerrtabs.selectionModel().select(1) + errorfield.scrollTop = Double.MaxValue + logfield.scrollTop = Double.MaxValue + new Alert(AlertType.Error) { + initOwner(stage) + title = "TeX Live Manager tlmgr has terminated" + headerText = "TeX Live Manager tlmgr has terminated - we cannot continue" + contentText = "Please inspect the debug output in the main window lower pane!" + }.showAndWait() + // Platform.exit() + // sys.exit(1) + } + case Failure(e) => + errorText += "lineUpdateFunc(stdout) thread got interrupted -- probably old tlmgr, ignoring it!" + e.printStackTrace + } + val stderrFuture = Future { + var alive = true + while (alive) { + val s = errorLine.take + if (s == null) + alive = false + else + stderrLineUpdateFunc(s) } } - bar.onComplete { - case Success(value) => println(s"Got the callback, meaning = $value") + stderrFuture.onComplete { + case Success(value) => // println(s"tlmgr stderr reader terminated: ${value}") case Failure(e) => - println("lineUpdateFunc thread got interrupted -- probably old tlmgr, ignoring it!") - //e.printStackTrace + errorText += "lineUpdateFunc(stderr) thread got interrupted -- probably old tlmgr, ignoring it!" + e.printStackTrace } tt } + def tlmgr_run_one_cmd(s: String, onCompleteFunc: (String, Array[String]) => Unit): Unit = { + currentPromise = Promise[(String, Array[String])]() + tlmgrBusy.value = true + currentPromise.future.onComplete { + case Success((a, b)) => + // println("DEBUG current future completed!") + Platform.runLater { + onCompleteFunc(a, b) + } + case Failure(ex) => + errorText += "Running a tlmgr command did not succeed: " + ex.getMessage + } + // println(s"DEBUG sending ${s}") + tlmgr.send_command(s) + } + + def tlmgr_send(s: String, onCompleteFunc: (String, Array[String]) => Unit): Unit = { + // logText.clear() + outputText.clear() + outerrpane.expanded = false + if (!currentPromise.isCompleted) { + // println(s"DEBUG tlmgr busy, put onto pending jobs: $s") + pendingJobs += ((s, onCompleteFunc)) + } else { + tlmgr_run_one_cmd(s, onCompleteFunc) + } + } + def reinitialize_tlmgr(): Unit = { tlmgr.cleanup() // Thread.sleep(1000) @@ -1128,37 +1300,47 @@ object ApplicationMain extends JFXApp { } def tlmgr_post_init():Unit = { - tlmgr.start_process() - tlmgr.get_output_till_prompt() - // update_pkg_lists_to_be_renamed() // this is async done - pkgs.clear() - upds.clear() - bkps.clear() - update_pkgs_lists() - } - - - /* TODO implement splash screen - see example in ProScalaFX - val startalert = new Alert(AlertType.Information) - startalert.setTitle("Loading package database ...") - startalert.setContentText("Loading package database, this might take a while. Please wait!") - startalert.show() - */ - - /* - var testmode = false - if (parameters.unnamed.nonEmpty) { - if (parameters.unnamed.head == "-test" || parameters.unnamed.head == "--test") { - println("Testing mode enabled, not actually calling tlmgr!") - testmode = true + if (!tlmgr.start_process()) { + println("Cannot start tlmgr process, terminating!") + Platform.exit() + sys.exit(1) } + + // check for tlmgr revision + tlmgr_send("version", (status,output) => { + output.foreach ( l => { + if (l.startsWith("revision ")) { + val tlmgrRev = l.stripPrefix("revision ") + if (tlmgrRev == "unknown") { + println("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() + load_tlpdb_update_pkgs_view() + }) } - */ - var lineUpdateFunc: String => Unit = { (l: String) => } // println(s"DEBUG: got ==$l== from tlmgr") } + def defaultStdoutLineUpdateFunc(l: String) : Unit = { } // println(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/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/LocationDialog.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/LocationDialog.scala new file mode 100644 index 00000000000..30088ff69a6 --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/LocationDialog.scala @@ -0,0 +1,117 @@ +package TLCockpit + +import TLCockpit.ApplicationMain.{not_implemented_info, stage} +import TeXLive.{OsTools, TLOption} + +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]) { + + 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 + + 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") { + 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)) + // println(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 + // println(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, new HPos(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)) => + // println("Got resutl " + foo) + Some(foo) + case Some(foo) => + // println("Got strange result " + foo) + None + case None => + None + } + } + +} diff --git a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/OptionsDialog.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/OptionsDialog.scala new file mode 100644 index 00000000000..ca7228da7ee --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/OptionsDialog.scala @@ -0,0 +1,129 @@ +package TLCockpit + +import TLCockpit.ApplicationMain.{not_implemented_info, stage} +import TeXLive.{OsTools, TLOption, TLPaperConf} + +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]) { + + 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, new HPos(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)) => + // println("Got resutl " + foo) + Some(foo) + case Some(foo) => + // println("Got strange result " + foo) + None + case None => + None + } + } + +} diff --git a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/PaperDialog.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/PaperDialog.scala new file mode 100644 index 00000000000..a195c23f654 --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/PaperDialog.scala @@ -0,0 +1,83 @@ +package TLCockpit + +import TLCockpit.ApplicationMain.stage +import TeXLive.TLPaperConf + +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]) { + + 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, new HPos(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)) => + // println("Got resutl " + foo) + Some(foo) + case Some(foo) => + // println("Got strange result " + foo) + None + case None => + None + } + } + +} diff --git a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/PkgInfoDialog.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/PkgInfoDialog.scala new file mode 100644 index 00000000000..3b5ea6fc848 --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/PkgInfoDialog.scala @@ -0,0 +1,145 @@ +package TLCockpit + +import TLCockpit.ApplicationMain.{tlpkgs,tlmgr,stage} +import TLCockpit.Utils._ +import TeXLive.OsTools + +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(pkg: String) 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 = tlpkgs(pkg).installed + val grid = new GridPane() { + hgap = 10 + vgap = 10 + padding = Insets(20) + } + + def do_one(k: String, v: String, row: Int): Int = { + grid.add(new Label(k), 0, row) + grid.add(new Label(v) { + wrapText = true + }, 1, row) + row + 1 + } + + var crow = 0 + val tlp = tlpkgs(pkg) + 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.map { _._2}.toList.foldLeft[Long](0)(_ + _) * 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 != None) + crow = do_one("cat-version", catdata.version.get, crow) + if (catdata.date != None) + crow = do_one("cat-date", catdata.date.get, crow) + if (catdata.license != None) + crow = do_one("cat-license", catdata.license.get, crow) + if (catdata.topics != None) + crow = do_one("cat-topics", catdata.topics.get, crow) + if (catdata.related != None) + crow = do_one("cat-related", catdata.related.get, crow) + // add files section + //println(tlpkgs(pkg)) + val docFiles = tlpkgs(pkg).docfiles + if (docFiles.nonEmpty) { + grid.add(new Label("doc files"), 0, crow) + grid.add(doListView(docFiles.map(s => s.file.replaceFirst("RELOC", "texmf-dist")), isInstalled), 1, crow) + crow += 1 + } + val runFiles = tlpkgs(pkg).runfiles + if (runFiles.nonEmpty) { + grid.add(new Label("run files"), 0, crow) + grid.add(doListView(runFiles.map(s => s.replaceFirst("RELOC", "texmf-dist")), false), 1, crow) + crow += 1 + } + val srcFiles = tlpkgs(pkg).srcfiles + if (srcFiles.nonEmpty) { + grid.add(new Label("src files"), 0, crow) + grid.add(doListView(srcFiles.map(s => s.replaceFirst("RELOC", "texmf-dist")), false), 1, crow) + crow += 1 + } + val binFiles = tlpkgs(pkg).binfiles + if (binFiles.nonEmpty) { + grid.add(new Label("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, new HPos(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(" ") + 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/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/TLDisplayClasses.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/TLDisplayClasses.scala new file mode 100644 index 00000000000..630cbfce061 --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/TLDisplayClasses.scala @@ -0,0 +1,30 @@ +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/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/Utils.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/Utils.scala new file mode 100644 index 00000000000..fb364f12f80 --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/Utils.scala @@ -0,0 +1,18 @@ +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)}" + } + +} diff --git a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/JsonProtocol.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/JsonProtocol.scala new file mode 100644 index 00000000000..3e28c582f2a --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/JsonProtocol.scala @@ -0,0 +1,15 @@ +package TeXLive + +import spray.json._ + + +object JsonProtocol extends DefaultJsonProtocol { + implicit val catalogueDataFormat = jsonFormat6(CatalogueData) + implicit val docfileFormat = jsonFormat3(DocFile) + 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/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/OsTools.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/OsTools.scala index 38d0c334d99..4590eb6b5c5 100644 --- a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/OsTools.scala +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/OsTools.scala @@ -5,7 +5,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import sys.process._ object OsTools { - val OS = System.getProperty("os.name").map(_.toLower) + val OS: String = System.getProperty("os.name").map(_.toLower) def isWindows: Boolean = { OS.startsWith("windows") } @@ -19,7 +19,7 @@ object OsTools { def openFileCmd(f: String): Seq[String] = { val absf = new java.io.File(f).getCanonicalPath if (isWindows) { - Seq("cmd", "start", absf) + Seq("cmd", "/c", "start", absf) } else if (isApple) { Seq("open", absf) } else { diff --git a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLBackup.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLBackup.scala deleted file mode 100644 index a00c1464f30..00000000000 --- a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLBackup.scala +++ /dev/null @@ -1,9 +0,0 @@ -package TeXLive - -import scalafx.beans.property.StringProperty - - -case class TLBackup(name: StringProperty, rev: StringProperty, date: StringProperty) { - def this(_name: String, _rev: String, _date: String) = - this(StringProperty(_name), StringProperty(_rev), StringProperty(_date)) -} diff --git a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLClasses.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLClasses.scala new file mode 100644 index 00000000000..c526ae50739 --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLClasses.scala @@ -0,0 +1,56 @@ +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 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/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLPackage.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLPackage.scala deleted file mode 100644 index 9a797d9c8ae..00000000000 --- a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLPackage.scala +++ /dev/null @@ -1,15 +0,0 @@ -package TeXLive - -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 TLPackage(name: StringProperty, lrev: ObjectProperty[Int], rrev: ObjectProperty[Int], - shortdesc: StringProperty, size: ObjectProperty[Int], 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) - ) -}
\ No newline at end of file diff --git a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLUpdate.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLUpdate.scala deleted file mode 100644 index 43623a523dd..00000000000 --- a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLUpdate.scala +++ /dev/null @@ -1,15 +0,0 @@ -package TeXLive - -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 TLUpdate(name: StringProperty, var status: StringProperty, 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/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TlmgrProcess.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TlmgrProcess.scala index 169f44154fb..7d84e6dd008 100644 --- a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TlmgrProcess.scala +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TlmgrProcess.scala @@ -2,103 +2,50 @@ package TeXLive import java.io._ -import OsTools._ +import TeXLive.OsTools._ import scala.collection.mutable.ArrayBuffer import scala.concurrent.SyncVar -// import scala.sys.process.{Process, ProcessBuilder, ProcessIO} import scala.sys.process._ -class TlmgrProcess(updout: Array[String] => Unit, upderr: String => Unit, updline: String => Unit) { +class TlmgrProcess(updout: String => Unit, upderr: String => Unit) { 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 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 = "kpsewhich -var-value SELFAUTOPARENT".!!.trim // println("tlroot ==" + tlroot + "==") // set in only one place, in the main thread var process: Process = _ - var lastInput: String = "" - var lastOk: Boolean = false - var isBusy = false - - def send_command(input: String): Array[String] = { - - lastInput = input - - val maxWaitTime = 5000 - var waited = 0 - var sendNL = 0 - while (isBusy) { - // do busy waiting here in case some other tlmgr command is running - // println("Debug: waiting for tlmgr being ready, want to send " + input) - Thread.sleep(300) - waited += 300 - - if (waited > maxWaitTime && sendNL < 3) { - outputString.put("protocol") - sendNL += 1 - } - if (waited > maxWaitTime && sendNL >= 3) { - throw new Exception("tlmgr busy, waited too long, aborting calling: " + input) - } - } + def send_command(input: String): Unit = { try { - // pass the input and wait for the output assert(!inputString.isSet) - assert(!outputString.isSet) - // errorBuffer.setLength(0) - synchronized(isBusy = true) inputString.put(input) - val ret = if (input != "quit") { - get_output_till_prompt() - } else { - new Array[String](0) - } - // updout(ret.mkString("\n")) - updout(ret) - upderr(errorBuffer.toString) - synchronized( isBusy = false ) - ret } catch { case exc: Throwable => - errorBuffer.append(" Main thread: " + + upderr("Main thread: " + (if (exc.isInstanceOf[NoSuchElementException]) "Timeout" else "Exception: " + exc)) - null } } - def start_process(): Unit = { + def isAlive(): Boolean = { + if (process != null) + process.isAlive() + else + // return true on not-started process + true + } + + def start_process(): Boolean = { // process creation if (process == null) { - val procIO = new ProcessIO(inputFn(_), outputFn(_), errorFn(_)) - val processBuilder: ProcessBuilder = Seq({if (isWindows) "tlmgr.bat" else "tlmgr"}, "--machine-readable", "shell") + val procIO = new ProcessIO(inputFn(_), outputFn(_, updout), outputFn(_, upderr)) + val processBuilder: ProcessBuilder = Seq({if (isWindows) "tlmgr.bat" else "tlmgr"}, "-v", "--machine-readable", "shell") process = processBuilder.run(procIO) } - } - - def get_output_till_prompt(): Array[String] = { - var ret = ArrayBuffer[String]() - var result = "" - var found = false - synchronized(isBusy = true) - while (!found) { - result = outputString.take() - if (result == "tlmgr> ") { - found = true - } else if (result == "OK") { - lastOk = true - // do nothing, we found a good ok - } else if (result == "ERROR") { - lastOk = false - } else { - ret += result - } - } - synchronized(isBusy = false) - ret.toArray + process.isAlive() } def cleanup(): Unit = { @@ -129,53 +76,29 @@ class TlmgrProcess(updout: Array[String] => Unit, upderr: String => Unit, updlin case exc: Throwable => stdin.close() // println("Exception in inputFn thread: " + exc + "\n") - errorBuffer.append(" Input thread: Exception: " + exc + "\n") + upderr("Input thread: Exception: " + exc + "\n") } } - private[this] def outputFn(stdOut: InputStream): Unit = { - val reader = new BufferedReader(new InputStreamReader(stdOut)) + 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 - if (line == null) { - // println("Did read NULL from stdin giving up") - // error = true - } else { - // println("DDD did read " + line + " from process") - outputString.put(line) - try { - updline(line) - } catch { - case exc: Throwable => - println("Update line function failed, continuing anyway (probably old tlmgr)!") - } + // println("DDD did read " + line + " from process") + try { + updfun(line) + } catch { + case exc: Throwable => + upderr("Update output line function failed, continuing anyway. Exception: " + exc) } } - stdOut.close() - } catch { - case exc: Throwable => - stdOut.close() - // println("Exception in outputFn thread: " + exc + "\n") - errorBuffer.append(" Output thread: Exception: " + exc + "\n") - } - } - - private[this] def errorFn(stdErr: InputStream): Unit = { - val reader = new BufferedReader(new InputStreamReader(stdErr)) - try { - var line = reader.readLine - while (line != null) { - errorBuffer.append(line + "\n") - line = reader.readLine - } - stdErr.close() + outStr.close() } catch { case exc: Throwable => - stdErr.close() - // println("Exception in errorFn thread: " + exc + "\n") - errorBuffer.append(" Error thread: Exception: " + exc + "\n") + outStr.close() + upderr("Output thread: Exception: " + exc + "\n") } } } diff --git a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/package.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/package.scala new file mode 100644 index 00000000000..88b3bb3e41a --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/package.scala @@ -0,0 +1,4 @@ + +package object TeXLive { + val tlBlockSize = 4096 // TL gives sizes in multiples of BlockSize! +} |