diff options
Diffstat (limited to 'Master')
8 files changed, 678 insertions, 550 deletions
diff --git a/Master/texmf-dist/doc/support/tlcockpit/README.md b/Master/texmf-dist/doc/support/tlcockpit/README.md index bd806ba995e..7d6d09b3e4c 100644 --- a/Master/texmf-dist/doc/support/tlcockpit/README.md +++ b/Master/texmf-dist/doc/support/tlcockpit/README.md @@ -1,35 +1,41 @@ TLCockpit - GUI frontend to tlmgr ================================= -TLCockpit aims at being a GUI for [tlmgr](https://www.tug.org/texlive/tlmgr.html), the [TeX Live](https://www.tug.org/texlive/) Manager, with a modern look and feel. +TLCockpit aims at being a GUI for [tlmgr](https://www.tug.org/texlive/tlmgr.html), +the [TeX Live](https://www.tug.org/texlive/) Manager, with a modern look and feel. -TLCockpit is included in TeX Live, but you need the latest version of tlmgr, see the secion on <em>Test versions</em> in the above document. Supported functionality ----------------------- -By now only loading of remote repository, listing installed, updated, -or all packages. Clicking on a package in the list opens a details -window. +While functionality is by now restricted to listing of packages, getting information +on packages, and updating/installing/removing packages, further features will be added +over time. -Furthermore, an input field allows entering arbitrary commands of the +For advanced users there is an input field allowing to enter arbitrary commands of the *tlmgr shell*, see [tlmgr manual](https://www.tug.org/texlive/doc/tlmgr.html#shell) for details. Installing/Running ------------------ -If you don't want to build your own deployment jar (which I don't recommend), -download [tlcockpit.jar](jar/tlcockpit.jar) and run it with +Since TLCockpit is included in TeX Live, it will be probably already installed +on your system. In case it isn't, a simple call to `tlmgr install tlcockpit` +should suffice. -``` -java -jar tlcockpit.jar -``` +We are providing new jar files on a daily bases in the github page, but update +the copy in TeX Live only at certain intervals. If you want to try out the latest +and greatest, get [tlcockpit.jar](jar/tlcockpit.jar) and put it into +`TEXLIVEROOT/temxf-dist/scripts/tlcockpit` replacing the current `tlcockpit.jar`. -The `tlmgr` program has to be in the PATH, and be the most current version. +After this you can run it by simply typing `tlcockpit`, or via a direct `java` +call as in `java -jar PATH/TO/tlcockpit.jar`. Requirements ------------ -A recent Java installation that included JavaFX is required. If you see +You need at least `tlmgr` from after 2017-09-01, in TeX Live language at least +version 45190 or `texlive.infra`. + +A recent Java installation that includes JavaFX is required. If you see errors like ``` Error: A JNI error has occurred, please check your installation and try again @@ -59,6 +65,8 @@ writing Scala and my style is horrible ;-) Screenshots ----------- +(permanently outdated) + Main window ![Main window](screenshots/mainwindow.png) diff --git a/Master/texmf-dist/scripts/tlcockpit/tlcockpit.jar b/Master/texmf-dist/scripts/tlcockpit/tlcockpit.jar Binary files differindex 1e1eb2f2996..8f7a587af2a 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/build.sbt b/Master/texmf-dist/source/support/tlcockpit/build.sbt index c5f7bbd251e..75d9d200771 100644 --- a/Master/texmf-dist/source/support/tlcockpit/build.sbt +++ b/Master/texmf-dist/source/support/tlcockpit/build.sbt @@ -1,6 +1,6 @@ name := "tlcockpit" -version := "0.1" +version := "0.2" scalaVersion := "2.12.3" @@ -11,5 +11,9 @@ libraryDependencies += "org.scalafx" % "scalafx_2.12" % "8.0.102-R11" mainClass in assembly := Some("TLCockpit.ApplicationMain") +assemblyJarName in assembly := "tlcockpit.jar" +assemblyOutputPath in assembly := file("jar/tlcockpit.jar") + + // for scalafx fork := true 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 new file mode 100644 index 00000000000..d7cad7021af --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TLCockpit/ApplicationMain.scala @@ -0,0 +1,482 @@ +// TLCockpit +// Copyright 2017 Norbert Preining +// Licensed according to GPLv3+ +// +// Front end for tlmgr + +package TLCockpit + +import TeXLive.{TLPackage, TlmgrProcess} + +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global +import scala.util.{Failure, Success} +import scalafx.geometry.HPos +import scalafx.scene.control.Alert.AlertType + +// needed see https://github.com/scalafx/scalafx/issues/137 +import scalafx.scene.control.TableColumn._ +import scalafx.scene.control.Menu._ +import scalafx.Includes._ +import scalafx.application.{JFXApp, Platform} +import scalafx.application.JFXApp.PrimaryStage +import scalafx.geometry.Insets +import scalafx.scene.Scene +import scalafx.scene.layout._ +import scalafx.scene.control._ +import scalafx.event.ActionEvent +import scalafx.collections.ObservableBuffer +import scala.sys.process._ +import scala.collection.mutable.ArrayBuffer + + +object ApplicationMain extends JFXApp { + + // necessary action when Window is closed with X or some other operation + override def stopApp(): Unit = { + tlmgr.cleanup() + } + + + val pkgs = ArrayBuffer[TLPackage]() + val viewpkgs = ObservableBuffer[TLPackage]() + + val outputfield = new TextArea { + editable = false + wrapText = true + } + val errorfield = new TextArea { + editable = false + wrapText = true + } + + val update_self_button = new Button { + text = "Update self" + disable = true + onAction = (e: ActionEvent) => callback_update_self() + } + val update_all_button = new Button { + text = "Update all" + disable = true + onAction = (e: ActionEvent) => callback_update_all() + } + + val outerrtabs = new TabPane { + minWidth = 400 + tabs = Seq( + new Tab { + text = "Output" + closable = false + content = outputfield + }, + new Tab { + text = "Error" + closable = false + content = errorfield + } + ) + } + val outerrpane = new TitledPane { + text = "Debug" + collapsible = true + expanded = false + content = outerrtabs + } + + val cmdline = new TextField() + val tlmgr = new TlmgrProcess((s:String) => outputfield.text = s, + (s:String) => { + errorfield.text = s + if (s != "") { + outerrpane.expanded = true + val selmod = outerrtabs.selectionModel() + selmod.select(1) + } + }) + + def tlmgr_async_command(s: String, f: Array[String] => Unit): Unit = { + errorfield.text = "" + outputfield.text = "" + outerrpane.expanded = false + val foo = Future { tlmgr.send_command(s) } + foo onComplete { + case Success(ret) => f(ret) + case Failure(t) => println("An ERROR has occurred: " + t.getMessage) + } + } + + def update_update_button_state(): Unit = { + update_all_button.disable = ! pkgs.foldLeft(false)( + (a,b) => a || (if (b.name.value == "texlive.infra") false else b.lrev.value.toInt > 0 && b.lrev.value.toInt < b.rrev.value.toInt) + ) + // TODO we should change pkgs to a Map with keys are the names + repository (for multi repo support) + // and the values are llrev/rrev or some combination + update_self_button.disable = ! pkgs.foldLeft(false)( + (a,b) => a || (if (b.name.value == "texlive.infra") b.lrev.value.toInt < b.rrev.value.toInt else false) + ) + } + + def callback_quit(): Unit = { + tlmgr.cleanup() + Platform.exit() + sys.exit(0) + } + + def callback_load_update_info(s:String, strs: Array[String]): Unit = { + if (s == "remote") { + tlmgr_async_command("info --data name,localrev,remoterev,shortdesc", (s: Array[String]) => { + val newpkgs = ArrayBuffer[TLPackage]() + 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("""\"""",""""""") + newpkgs += new TLPackage(fields(0), fields(1), fields(2), shortdesc) + }) + pkgs.clear() + newpkgs.map(pkgs += _) + viewpkgs.clear() + pkgs.map(viewpkgs += _) + // check for updates available + update_update_button_state() + }) + } + } + + def callback_load(s: String): Unit = { + tlmgr_async_command("load " + s, callback_load_update_info(s, _) ) + } + + def callback_run_text(s: String): Unit = { + tlmgr_async_command(s, (s: Array[String]) => {}) + // tlmgr_command(s) + } + def callback_run_cmdline(): Unit = { + callback_run_text(cmdline.text.value) + } + + def callback_list_collections(): Unit = { + tlmgr_async_command("info collections", foo => { + val foo: Array[String] = Array() + // println("got result from info collections: ") + // foo.map(println(_)) + outputfield.text = foo.mkString("\n") + errorfield.text = tlmgr.errorBuffer.toString + }) + } + + def not_implemented_info(): Unit = { + new Alert(AlertType.Warning) { + initOwner(stage) + title = "Warning" + headerText = "This functionality is not implemented by now!" + contentText = "Sorry for the inconveniences." + }.showAndWait() + } + + def callback_run_external(s: String): Unit = { + val foo = Future { s.!! } + foo.onComplete { + case Success(ret) => outputfield.text = ret.mkString("\n") + case Failure(t) => println("An ERROR has occurred: " + t.getMessage) + } + } + + def callback_about(): Unit = { + new Alert(AlertType.Information) { + initOwner(stage) + title = "About TLCockpit" + headerText = "TLCockpit version 0.2\nLicense: GPL3+" + contentText = "Brought to you by Norbert\nSources: https://github.com/TeX-Live/tlcockpit" + }.showAndWait() + } + def callback_show_all() : Unit = { + viewpkgs.clear() + pkgs.map(viewpkgs += _) + } + def callback_show_installed() : Unit = { + viewpkgs.clear() + pkgs.map(p => if (p.lrev.value.toInt > 0) viewpkgs += p) + } + def callback_show_updates(): Unit = { + val newpkgs = ArrayBuffer[TLPackage]() + pkgs.map(p => + // TODO we are not dealing with not integer values by now! + if (p.lrev.value.toInt > 0 && p.lrev.value.toInt < p.rrev.value.toInt) { + newpkgs += p + } + ) + viewpkgs.clear() + newpkgs.map(viewpkgs += _) + } + def callback_update_all() : Unit = { + tlmgr_async_command("update --all", s => { + update_update_button_state() + callback_show_all() + }) + } + def callback_update_self() : Unit = { + // TODO + // should we restart tlmgr here - it might be necessary!!! + tlmgr_async_command("update --self", s => { + update_update_button_state() + callback_show_all() + }) + } + + + def callback_show_pkg_info(pkg: String): Unit = { + // val pkginfo = tlmgr_command(s"info $pkg") + tlmgr_async_command(s"info $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 grid = new GridPane() { + hgap = 10 + vgap = 10 + padding = Insets(20) + } + var crow = 0 + pkginfo.foreach((line: String) => { + val keyval = line.split(":", 2).map(_.trim) + if (keyval.length == 2) { + 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 + } + }) + grid.columnConstraints = Seq(new ColumnConstraints(100, 150, 200), new ColumnConstraints(100, 300, 5000, Priority.Always, new HPos(HPos.Left), true)) + dialog.dialogPane().content = grid + dialog.width = 500 + dialog.showAndWait() + } + }) + } + + 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 (!testmode) { + tlmgr.start_process() + + // wait until we got a prompt + val foo: Array[String] = tlmgr.get_output_till_prompt() + // println("current output: ") + // foo.map(println(_)) + } + + stage = new PrimaryStage { + title = "TLCockpit" + scene = new Scene { + root = { + val topBox = new MenuBar { + useSystemMenuBar = true + menus.add(new Menu("TLCockpit") { + items = List( + new MenuItem("Load default (from tlpdb) repository") { + onAction = (ae: ActionEvent) => callback_load("remote") + }, + new MenuItem("Load standard net repository") { + onAction = (ae: ActionEvent) => not_implemented_info() + disable = true + }, + new MenuItem("Load other repository ...") { + onAction = (ae: ActionEvent) => not_implemented_info() + disable = true + }, + new SeparatorMenuItem, + new MenuItem("Exit") { + onAction = (ae: ActionEvent) => callback_quit() + } + ) + }) + menus.add(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() }, + 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 } + ) + }) + menus.add(new Menu("Actions") { + items = List( + new MenuItem("Update filename database ...") { onAction = (ae) => callback_run_external("mktexlsr") }, + new MenuItem("Rebuild all formats ...") { onAction = (ae) => callback_run_external("fmtutil --sys --all") }, + 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() } + ) + }) + menus.add(new Menu("Help") { + items = List( + new MenuItem("Manual") { disable = true; onAction = (ae) => not_implemented_info() }, + new MenuItem("About") { onAction = (ae) => callback_about() }, + ) + }) + } + val centerBox = new VBox { + padding = Insets(10) + children = List( + new TitledPane { + text = "Actions" + collapsible = false + content = new VBox { + spacing = 10 + children = List( + new HBox { + spacing = 10 + children = List( + cmdline, + new Button { + text = "Go" + onAction = (event: ActionEvent) => callback_run_cmdline() + }, + new Button { + text = "Load repository"; + onAction = (event: ActionEvent) => callback_load("remote") + } + ) + }, + new HBox { + spacing = 10 + children = List( + new Button { + text = "Show updates" + onAction = (e: ActionEvent) => callback_show_updates() + }, + new Button { + text = "Show installed" + onAction = (e: ActionEvent) => callback_show_installed() + }, + new Button { + text = "Show all" + onAction = (e: ActionEvent) => callback_show_all() + }, + update_self_button, + update_all_button + ) + } + ) + } + }, + outerrpane, + { + val col1 = new TableColumn[TLPackage, String] { + text = "Package" + cellValueFactory = { + _.value.name + } + prefWidth = 150 + } + val col2 = new TableColumn[TLPackage, String] { + text = "Local rev" + cellValueFactory = { + _.value.lrev + } + prefWidth = 100 + } + val col3 = new TableColumn[TLPackage, String] { + text = "Remote rev" + cellValueFactory = { + _.value.rrev + } + prefWidth = 100 + } + val col4 = new TableColumn[TLPackage, String] { + text = "Description" + cellValueFactory = { + _.value.shortdesc + } + prefWidth = 300 + } + val table = new TableView[TLPackage](viewpkgs) { + columns ++= List(col1,col2,col3,col4) + } + col4.prefWidth.bind(table.width - col1.width - col2.width - col3.width) + table.prefHeight = 300 + table.vgrow = Priority.Always + table.rowFactory = { _ => + val row = new TableRow[TLPackage] {} + 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) + }, + new MenuItem("Remove") { + onAction = (ae) => callback_run_text("remove " + row.item.value.name.value) + }, + new MenuItem("Update") { + onAction = (ae) => callback_run_text("update " + row.item.value.name.value) + } + ) + row.contextMenu = ctm + row + } + table + }, + ) + } + /* val bottomBox = new HBox { + padding = Insets(10) + spacing = 10 + children = List( + new Button { + text = "Quit" + onAction = (event: ActionEvent) => callback_quit() + } + ) + } + */ + new BorderPane { + // padding = Insets(20) + top = topBox + // left = leftBox + center = centerBox + // bottom = bottomBox + } + } + } + } + + 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() + + // load the initial set of packages + var pkglines: Array[String] = Array() + + tlmgr_async_command("info --only-installed --data name,localrev,shortdesc", (s: Array[String]) => { + s.map(line => { + val fields: Array[String] = line.split(",", -1) + val sd = if (fields(2).isEmpty) "" else fields(2).substring(1).dropRight(1).replace("""\"""",""""""") + pkgs += new TLPackage(fields(0), fields(1), "0", sd) + }) + viewpkgs.clear() + pkgs.map(viewpkgs += _) + Platform.runLater { startalert.close() } + }) + + +} // object ApplicationMain + +// vim:set tabstop=2 expandtab : // diff --git a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/OsInfo.scala b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/OsInfo.scala new file mode 100644 index 00000000000..3b5ad20864f --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/OsInfo.scala @@ -0,0 +1,8 @@ +package TeXLive + +object OsInfo { + val OS = System.getProperty("os.name") + def isWindows: Boolean = { + OS.startsWith("Windows") + } +} 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 new file mode 100644 index 00000000000..1f79744fc7f --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TLPackage.scala @@ -0,0 +1,10 @@ +package TeXLive + +import scalafx.beans.property.StringProperty + +class TLPackage(name_ : String, lrev_ : String, rrev_ : String, shortdesc_ : String) { + val name = new StringProperty(this, "name", name_) + val shortdesc = new StringProperty(this, "shortdesc", shortdesc_) + val lrev = new StringProperty(this, "revision", lrev_) + val rrev = new StringProperty(this, "revision", rrev_) +} 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 new file mode 100644 index 00000000000..be1a8cd8290 --- /dev/null +++ b/Master/texmf-dist/source/support/tlcockpit/src/main/scala/TeXLive/TlmgrProcess.scala @@ -0,0 +1,152 @@ +package TeXLive + +import java.io._ + +import OsInfo._ + +import scala.collection.mutable.ArrayBuffer +import scala.concurrent.SyncVar +import scala.sys.process.{Process, ProcessBuilder, ProcessIO} + +// TODO line by line update of stdout/stderr via an update function passed in +// otherwise we don't see any update while a long running command like update --all +// is executed! +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 + + // set in only one place, in the main thread + var process: Process = _ + var lastInput: String = "" + var lastOk: Boolean = false + + def send_command(input: String): Array[String] = { + + lastInput = input + + try { + // pass the input and wait for the output + assert(!inputString.isSet) + assert(!outputString.isSet) + errorBuffer.setLength(0) + inputString.put(input) + var ret = if (input != "quit") { + get_output_till_prompt() + } else { + new Array[String](0) + } + updout(ret.mkString("\n")) + upderr(errorBuffer.toString) + ret + } catch { + case exc: Throwable => + errorBuffer.append(" Main thread: " + + (if (exc.isInstanceOf[NoSuchElementException]) "Timeout" else "Exception: " + exc)) + null + } + } + + def start_process(): Unit = { + // 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") + process = processBuilder.run(procIO) + } + } + + def get_output_till_prompt(): Array[String] = { + var ret = ArrayBuffer[String]() + var result = "" + var found = false + 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 + } + } + ret.toArray + } + + def cleanup(): Unit = { + if (process != null) { + send_command("quit") + process.destroy() + // we'll need to unblock the input again + // TODO what is that needed for??? + // if (!inputString.isSet) inputString.put("") + // if (outputString.isSet) outputString.take() + } + } + + /* The standard input passing function */ + private[this] def inputFn(stdin: OutputStream): Unit = { + val writer = new BufferedWriter(new OutputStreamWriter(stdin)) + try { + var input = "" + while (true) { + input = inputString.take() + if (input == "quit") { + stdin.close() + return + } else { + writer.write(input + "\n") + // println("writing " + input + " to process") + writer.flush() + } + } + stdin.close() + } catch { + case exc: Throwable => + stdin.close() + errorBuffer.append(" Input thread: Exception: " + exc + "\n") + } + } + + private[this] def outputFn(stdOut: InputStream): Unit = { + val reader = new BufferedReader(new InputStreamReader(stdOut)) + val buffer: StringBuilder = new StringBuilder() + try { + var line: String = "" + while (true) { + line = reader.readLine + if (line == null) { + // println("Did read NULL from stdin giving up") + // error = true + } else { + // println("did read " + line + " from process") + outputString.put(line) + } + } + stdOut.close() + } catch { + case exc: Throwable => + stdOut.close() + 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() + } catch { + case exc: Throwable => + stdErr.close() + errorBuffer.append(" Error thread: Exception: " + exc + "\n") + } + } +} 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 deleted file mode 100644 index a12d5bad2ed..00000000000 --- a/Master/texmf-dist/source/support/tlcockpit/src/main/scala/tlcockpit/ApplicationMain.scala +++ /dev/null @@ -1,536 +0,0 @@ -// TLCockpit -// Copyright 2017 Norbert Preining -// Licensed according to GPLv3+ -// -// Front end for tlmgr - -package TLCockpit - -// import java.util.Date - -import java.io.InputStream -import java.io.OutputStream -import java.io.InputStreamReader -import java.io.OutputStreamWriter -import java.io.BufferedWriter -import java.io.BufferedReader - -import scalafx.beans.property.ObjectProperty -import scalafx.geometry.HPos -import scalafx.scene.control.Alert.AlertType - -// needed see https://github.com/scalafx/scalafx/issues/137 -import scalafx.scene.control.TableColumn._ -import scalafx.scene.control.Menu._ -import scalafx.Includes._ -import scalafx.application.{JFXApp, Platform} -import scalafx.application.JFXApp.PrimaryStage -import scalafx.geometry.Insets -import scalafx.scene.Scene -import scalafx.scene.layout._ -import scalafx.scene.control._ -import scalafx.event.ActionEvent -import scalafx.collections.ObservableBuffer -import scalafx.beans.property.StringProperty -import scalafx.scene.input.{KeyCode, KeyEvent, MouseEvent} - -//import com.sun.xml.internal.bind.WhiteSpaceProcessor - -import scala.sys.process._ -import scala.concurrent.SyncVar -import scala.collection.mutable.ArrayBuffer -import scala.collection.mutable.ArrayBuilder - -import scala.io.Source -//import scala.swing._ - - -class TLPackage(name_ : String, lrev_ : String, rrev_ : String, shortdesc_ : String) { - val name = new StringProperty(this, "name", name_) - val shortdesc = new StringProperty(this, "shortdesc", shortdesc_) - val lrev = new StringProperty(this, "revision", lrev_) - val rrev = new StringProperty(this, "revision", rrev_) -} - - -object OsInfo { - val OS = System.getProperty("os.name") - def isWindows: Boolean = { - OS.startsWith("Windows") - } -} - -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 - - // set in only one place, in the main thread - var process: Process = _ - var lastInput: String = "" - var lastOk: Boolean = false - - def send_command(input: String): Array[String] = { - - lastInput = input - - try { - // pass the input and wait for the output - assert(!inputString.isSet) - assert(!outputString.isSet) - inputString.put(input) - var ret = if (input != "quit") { - get_output_till_prompt() - } else { - new Array[String](0) - } - updout(ret.mkString("\n")) - upderr(errorBuffer.toString) - ret - } catch { - case exc: Throwable => - errorBuffer.append(" Main thread: " + - (if (exc.isInstanceOf[NoSuchElementException]) "Timeout" else "Exception: " + exc)) - null - } - } - - def start_process(): Unit = { - // process creation - if (process == null) { - val procIO = new ProcessIO(inputFn(_), outputFn(_), errorFn(_)) - val processBuilder: ProcessBuilder = Seq({if (OsInfo.isWindows) "tlmgr.bat" else "tlmgr"}, "--machine-readable", "shell") - process = processBuilder.run(procIO) - } - } - - def get_output_till_prompt(): Array[String] = { - var ret = ArrayBuffer[String]() - var result = "" - var found = false - 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 - } - } - ret.toArray - } - - def cleanup(): Unit = { - if (process != null) { - send_command("quit") - process.destroy() - // we'll need to unblock the input again - // TODO what is that needed for??? - // if (!inputString.isSet) inputString.put("") - // if (outputString.isSet) outputString.take() - } - } - - /* The standard input passing function */ - private[this] def inputFn(stdin: OutputStream): Unit = { - val writer = new BufferedWriter(new OutputStreamWriter(stdin)) - try { - var input = "" - while (true) { - input = inputString.take() - if (input == "quit") { - stdin.close() - return - } else { - writer.write(input + "\n") - // println("writing " + input + " to process") - writer.flush() - } - } - stdin.close() - } catch { - case exc: Throwable => - stdin.close() - errorBuffer.append(" Input thread: Exception: " + exc + "\n") - } - } - - private[this] def outputFn(stdOut: InputStream): Unit = { - val reader = new BufferedReader(new InputStreamReader(stdOut)) - val buffer: StringBuilder = new StringBuilder() - try { - var line: String = "" - while (true) { - line = reader.readLine - if (line == null) { - // println("Did read NULL from stdin giving up") - // error = true - } else { - // println("did read " + line + " from process") - outputString.put(line) - } - } - stdOut.close() - } catch { - case exc: Throwable => - stdOut.close() - 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() - } catch { - case exc: Throwable => - stdErr.close() - errorBuffer.append(" Error thread: Exception: " + exc + "\n") - } - } -} - -object ApplicationMain extends JFXApp { - - var testmode = false - if (parameters.unnamed.length > 0) { - if (parameters.unnamed(0) == "-test" || parameters.unnamed(0) == "--test") { - println("Testing mode enabled, not actually calling tlmgr!") - testmode = true - } - } - - val pkgs = ArrayBuffer[TLPackage]() - val viewpkgs = ObservableBuffer[TLPackage]() - - - def onQuit(event: ActionEvent): Unit = Platform.exit() - - val outputfield = new TextArea { - editable = false - wrapText = true - } - val errorfield = new TextArea { - editable = false - wrapText = true - } - val cmdline = new TextField() - val tlmgr = new TlmgrProcess((s:String) => outputfield.text = s, - (s:String) => errorfield.text = s) - - if (!testmode) { - tlmgr.start_process() - - // wait until we got a prompt - val foo: Array[String] = tlmgr.get_output_till_prompt() - // println("current output: ") - // foo.map(println(_)) - } - - // load the initial set of packages - val pkglines = if (testmode) - Array("aa,0,1,ffobar", "bb,4,5,fafaf") - else - tlmgr.send_command("info --only-installed --data name,localrev,shortdesc") - - pkglines.map(line => { - val fields: Array[String] = line.split(",",-1) - val sd = if (fields(2).isEmpty) "" else fields(2).substring(1).dropRight(1).replace("""\"""",""""""") - pkgs += new TLPackage(fields(0),fields(1),"0",sd) - }) - viewpkgs.clear() - pkgs.map(viewpkgs += _) - - def callback_quit(): Unit = { - tlmgr.cleanup() - Platform.exit() - sys.exit(0) - } - - def callback_load(s: String): Unit = { - val foo = tlmgr.send_command("load " + s) - if (s == "remote") { - val pkglines: Array[String] = tlmgr.send_command("info --data name,localrev,remoterev,shortdesc") - val newpkgs = ArrayBuffer[TLPackage]() - pkglines.map(line => { - val fields: Array[String] = line.split(",", -1) - val sd = fields(3) - val shortdesc = if (sd.isEmpty) "" else sd.substring(1).dropRight(1).replace("""\"""",""""""") - newpkgs += new TLPackage(fields(0), fields(1), fields(2), shortdesc) - }) - pkgs.clear() - newpkgs.map(pkgs += _) - viewpkgs.clear() - pkgs.map(viewpkgs += _) - } - // println("got result from load " + s + ": ") - // foo.map(println(_)) - } - - def callback_run_cmdline(): Unit = { - tlmgr.send_command(cmdline.text.value) - } - - def callback_list_collections(): Unit = { - val foo = tlmgr.send_command("info collections") - // println("got result from info collections: ") - // foo.map(println(_)) - outputfield.text = foo.mkString("\n") - errorfield.text = tlmgr.errorBuffer.toString - } - - def not_implemented_info(): Unit = { - new Alert(AlertType.Warning) { - initOwner(stage) - title = "Warning" - headerText = "This functionality is not implemented by now!" - contentText = "Sorry for the inconveniences." - }.showAndWait() - } - - def callback_about(): Unit = { - new Alert(AlertType.Information) { - initOwner(stage) - title = "About TLCockpit" - headerText = "TLCockpit version 0.1\nLicense: GPL3+" - contentText = "Brought to you by Norbert\nSources: https://github.com/TeX-Live/tlcockpit" - }.showAndWait() - } - def callback_show_all() : Unit = { - viewpkgs.clear() - pkgs.map(viewpkgs += _) - } - def callback_show_installed() : Unit = { - viewpkgs.clear() - pkgs.map(p => if (p.lrev.value.toInt > 0) viewpkgs += p) - } - def callback_show_updates(): Unit = { - val newpkgs = ArrayBuffer[TLPackage]() - pkgs.map(p => - // TODO we are not dealing with not integer values by now! - if (p.lrev.value.toInt > 0 && p.lrev.value.toInt < p.rrev.value.toInt) { - newpkgs += p - } - ) - viewpkgs.clear() - newpkgs.map(viewpkgs += _) - } - def callback_show_pkg_info(pkg: String): Unit = { - val pkginfo = tlmgr.send_command(s"info $pkg") - 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 grid = new GridPane() { - hgap = 10 - vgap = 10 - padding = Insets(20) - } - var crow = 0 - pkginfo.map((line:String) => { - val keyval = line.split(":",2).map(_.trim) - if (keyval.length == 2) { - 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 - } - }) - grid.columnConstraints = Seq(new ColumnConstraints(100,150,200),new ColumnConstraints(100,300,5000,Priority.Always,new HPos(HPos.Left),true)) - dialog.dialogPane().content = grid - dialog.width = 500 - dialog.showAndWait() - } - stage = new PrimaryStage { - title = "TLCockpit" - scene = new Scene { - root = { - val topBox = new MenuBar { - useSystemMenuBar = true - menus.add(new Menu("TLCockpit") { - items = List( - new MenuItem("Load default (from tlpdb) repository") { - onAction = (ae: ActionEvent) => callback_load("remote") - }, - new MenuItem("Load standard net repository") { - onAction = (ae: ActionEvent) => not_implemented_info() - }, - new MenuItem("Load other repository ...") { - onAction = (ae: ActionEvent) => not_implemented_info() - }, - new SeparatorMenuItem, - new MenuItem("Exit") { - onAction = (ae: ActionEvent) => callback_quit() - } - ) - }) - menus.add(new Menu("Options") { - items = List( - new MenuItem("General ...") { onAction = (ae) => not_implemented_info() }, - new MenuItem("Paper ...") { onAction = (ae) => not_implemented_info() }, - new MenuItem("Platforms ...") { onAction = (ae) => not_implemented_info() }, - new SeparatorMenuItem, - new CheckMenuItem("Expert options"), - new CheckMenuItem("Enable debugging options"), - new CheckMenuItem("Disable auto-install of new packages"), - new CheckMenuItem("Disable auto-removal of server-deleted packages") - ) - }) - menus.add(new Menu("Actions") { - items = List( - new MenuItem("Update filename database ...") { onAction = (ae) => not_implemented_info() }, - new MenuItem("Rebuild all formats ...") { onAction = (ae) => not_implemented_info() }, - new MenuItem("Update font map database ...") { onAction = (ae) => not_implemented_info() }, - new MenuItem("Restore packages from backup ...") { onAction = (ae) => not_implemented_info() }, - new MenuItem("Handle symlinks in system dirs ...") { onAction = (ae) => not_implemented_info() }, - new SeparatorMenuItem, - new MenuItem("Remove TeX Live ...") { onAction = (ae) => not_implemented_info() } - ) - }) - menus.add(new Menu("Help") { - items = List( - new MenuItem("Manual") { onAction = (ae) => not_implemented_info() }, - new MenuItem("About") { onAction = (ae) => callback_about() }, - ) - }) - } - val centerBox = new VBox { - children = List( - new TitledPane { - text = "Actions" - collapsible = false - content = new VBox { - spacing = 10 - children = List( - new HBox { - spacing = 10 - children = List( - cmdline, - new Button { - text = "Go" - onAction = (event: ActionEvent) => callback_run_cmdline() - }, - new Button { - text = "Load repository"; - onAction = (event: ActionEvent) => callback_load("remote") - } - ) - }, - new HBox { - spacing = 10 - children = List( - new Button { - text = "Show updates" - onAction = (e: ActionEvent) => callback_show_updates() - }, - new Button { - text = "Show installed" - onAction = (e: ActionEvent) => callback_show_installed() - }, - new Button { - text = "Show all" - onAction = (e: ActionEvent) => callback_show_all() - } - ) - } - ) - } - }, - new TitledPane { - text = "Debug" - collapsible = true - expanded = false - content = new TabPane { - minWidth = 400 - tabs = Seq( - new Tab { - text = "Output" - closable = false - content = outputfield - }, - new Tab { - text = "Error" - closable = false - content = errorfield - } - ) - } - }, { - val col1 = new TableColumn[TLPackage, String] { - text = "Package" - cellValueFactory = { - _.value.name - } - prefWidth = 150 - } - val col2 = new TableColumn[TLPackage, String] { - text = "Local rev" - cellValueFactory = { - _.value.lrev - } - prefWidth = 100 - } - val col3 = new TableColumn[TLPackage, String] { - text = "Remote rev" - cellValueFactory = { - _.value.rrev - } - prefWidth = 100 - } - val col4 = new TableColumn[TLPackage, String] { - text = "Description" - cellValueFactory = { - _.value.shortdesc - } - prefWidth = 300 - } - val table = new TableView[TLPackage](viewpkgs) { - columns ++= List(col1,col2,col3,col4) - } - col4.prefWidth.bind(table.width - col1.width - col2.width - col3.width) - table.rowFactory = { _ => - val row = new TableRow[TLPackage] {} - row.onMouseClicked = { (me: MouseEvent) => callback_show_pkg_info(row.item.value.name.value) } - row - } - table - }, - ) - } - val bottomBox = new HBox { - spacing = 10 - children = List( - new Button { - text = "Quit" - onAction = (event: ActionEvent) => callback_quit() - } - ) - } - - new BorderPane { - padding = Insets(20) - top = topBox - // left = leftBox - center = centerBox - bottom = bottomBox - } - } - } - } -} // object ApplicationMain - -// vim:set tabstop=2 expandtab : // |