VTE 2.90 Widget Example

This example demonstrates use of the VTE 2.90 widget API in an application.

The most recent VTE branch that supports API version 2.90 is 0.36. Subsequent versions of VTE (0.38 onwards) use API version 2.91 which is not compatible. See VteApp2 for an example using API version 2.91.

Also, this example demonstrates use of other GTK widgets:

Download

VteApp1.tar.gz

See Using Make for build instructions.

Library dependencies

  • GLib 2.0 (GLib, GObject, Gio)

  • GTK 3.0 (Gdk, Gtk)

  • VTE 2.90 (Vte)

File listings

vteapp.sml

fun activate app () =
  let
    val window = VteMainWindow.new app
    val () = Gtk.Window.present window
  in
    ()
  end

fun main () =
  let
    val appId = "org.giraffelibrary.demo.vteapp1"
    val app = Gtk.Application.new (SOME appId, Gio.ApplicationFlags.flags [])
    val id = Signal.connect app (Gio.Application.activateSig, activate app)

    val argv = Utf8CPtrArrayN.fromList (CommandLine.name () :: CommandLine.arguments ())
    val status = Gio.Application.run app argv

    val () = Signal.handlerDisconnect app id
  in
    Giraffe.exit status
  end
    handle e => Giraffe.error 1 ["Uncaught exception\n", exnMessage e, "\n"]

VteMainWindow.sml

structure VteMainWindow =
  struct
    open Gtk


    fun log level msg = GLib.log ("VteApp", level, msg)
    val logMessage = log GLib.LogLevelFlags.LEVEL_MESSAGE
    val logWarning = log GLib.LogLevelFlags.LEVEL_WARNING


    fun parseCheckColor name =
      let
        val color = Gdk.Color.parse name
        val () =
          case color of
            NONE => logWarning (concat ["colour \"", name, "\" not known"])
          | _    => ()
      in
        color
      end


    fun runWarnDlg parent title msg =
      let
        val dlg =
          GObject.Object.new (
            MessageDialogClass.t,
            [
              Property.init Window.titleProp              (SOME title),
              Property.init Window.transientForProp       (SOME parent),
              Property.init Window.modalProp              true,
              Property.init MessageDialog.messageTypeProp MessageType.WARNING,
              Property.init MessageDialog.buttonsProp     ButtonsType.OK,
              Property.init MessageDialog.textProp        (SOME (concat msg))
            ]
          )

        fun onResponse _ = Widget.destroy dlg
        val _ = Signal.connect dlg (Dialog.responseSig, onResponse)

        val () = Window.setModal dlg true
        val () = Widget.show dlg
      in
        ()
      end


    type proc = {
      pid : GLib.Pid.t
    }

    val theProc : proc option ref = ref NONE


    fun setWidgetProps {vte, cmdEntry, execBtn, killBtn} running =
      if running
      then (
        Widget.setCanFocus vte true;
        Widget.grabFocus vte;
        Widget.setSensitive cmdEntry false;
        Widget.setSensitive execBtn false;
        Widget.setSensitive killBtn true
      )
      else (
        Widget.setSensitive cmdEntry true;
        Widget.setSensitive execBtn true;
        Widget.setSensitive killBtn false;
        Widget.grabFocus cmdEntry;
        Widget.setCanFocus vte false
      )


    fun childClose (widgets as {vte, ...}) () =
      let
        val status = VteTerminal.getChildExitStatus vte
      in
        logMessage (
          concat [
            "childClose: application exited with status ",
            LargeInt.toString status
          ]
        );
        theProc := NONE;
        setWidgetProps widgets false
      end


    fun kill () =
      case !theProc of
        SOME {pid, ...} => (
          Posix.Process.kill (Posix.Process.K_GROUP pid, Posix.Signal.kill)
        )
      | NONE            => logMessage "Application not running"


    (* ---------------------------------------------------------------------- *
     * Accelerators                                                           *
     * ---------------------------------------------------------------------- *)

    fun makeAccel (name, accels) = (name, Utf8CPtrArray.fromList accels)
    fun makeAccels () =
      List.map makeAccel [
        ("app.quit", ["<control>q"])
      ]

    fun addAccels app =
      List.app (Application.setAccelsForAction app) (makeAccels ())


    (* ---------------------------------------------------------------------- *
     * Actions                                                                *
     * ---------------------------------------------------------------------- *)

    fun cmdExec mainWnd widgets () =
      case !theProc of
        SOME _ => logMessage "Application already started"
      | NONE   =>
          let
            val argv = GLib.shellParseArgv (Entry.getText (#cmdEntry widgets))

            val () =
              logMessage (
                concat [
                  "About to execute \"",
                  String.concatWith " " (Utf8CPtrArray.toList argv),
                  "\""
                ]
              )

            val pid =
              VteTerminal.forkCommandFull (#vte widgets) (
                VtePtyFlags.DEFAULT,
                NONE,
                argv,
                NONE,
                GLib.SpawnFlags.SEARCH_PATH
              )
          in
            theProc := SOME {pid = pid};
            setWidgetProps widgets true
          end
            handle
              GLib.Error (dom, err) => (
                logMessage (
                  case dom of
                    GLib.ShellError _ => "Failed to parse command"
                  | GLib.SpawnError _ => "Application failed to start"
                  | _                 => "Error with unknown origin"
                );
                runWarnDlg mainWnd "Error" [#get GLib.Error.message err]
              )

    fun cmdKill () = kill ()

    fun cmdFont mainWnd {vte, ...} () =
      let
        val dlg = FontChooserDialog.new (SOME "Choose font", SOME mainWnd)

        val () =
          FontChooser.setFontDesc (FontChooserDialog.asFontChooser dlg)
            (Vte.Terminal.getFont vte)

        fun onResponse res =
          let
            val () =
              if res = ResponseType.OK
              then
                Vte.Terminal.setFont vte
                  (FontChooser.getFontDesc (FontChooserDialog.asFontChooser dlg))
              else
                ()
 
            val () = Widget.destroy dlg
          in
            ()
          end
        val _ = Signal.connect dlg (Dialog.responseSig, onResponse)

        val () = Window.setModal dlg true
        val () = Widget.show dlg
      in
        ()
      end

    fun cmdQuit mainWnd () = Widget.destroy mainWnd

    fun addSimpleAction actionMap (name, activateFun : (unit -> unit) option) =
      let
        open Gio

        val action = SimpleAction.new (name, NONE)
        fun check f =
          fn
            NONE   => f ()
          | SOME _ =>
              log GLib.LogLevelFlags.LEVEL_WARNING
                "activate function expected argument NONE"

        val () =
          case activateFun of
            SOME f => ignore (
              Signal.connect action (SimpleAction.activateSig, check f)
            )
          | NONE   => ()
        val () = SimpleAction.setEnabled action true
        val () = ActionMap.addAction actionMap (SimpleAction.asAction action)
      in
        ()
      end


    (* ---------------------------------------------------------------------- *
     * Main window initialization                                             *
     * ---------------------------------------------------------------------- *)

    fun deleteEvent _ = false

    fun destroy app () = (
      case !theProc of
        SOME _ => kill ()
      | _      => ();
      Gio.Application.quit app
    )

    fun new app =
      let
        val spinLbl = Label.new (SOME "Scrollback lines:")
        val spinBtn = SpinButton.newWithRange (0.0, 999999999.0, 1.0)
        val cmdLbl = Label.new (SOME "Command:")
        val cmdEntry = Entry.new ()
        val execBtn = Button.newWithMnemonic "_Execute"
        val killBtn = Button.newWithMnemonic "_Kill"
        val fontBtn = Button.newWithMnemonic "_Font"
        val quitBtn = Button.newWithMnemonic "_Quit"
        val vte = VteTerminal.new ()
        val widgets = {
          cmdEntry = cmdEntry,
          execBtn  = execBtn,
          killBtn  = killBtn,
          vte      = vte
        }

        val hBox = Box.new (Orientation.HORIZONTAL, 0)
        val vBox = Box.new (Orientation.VERTICAL, 0)
        val scrWnd = ScrolledWindow.new (NONE, NONE)
        val mainWnd = ApplicationWindow.new app

        (* main window signals *)
        val _ = Signal.connect mainWnd (Widget.deleteEventSig, deleteEvent)
        val _ = Signal.connect mainWnd (Widget.destroySig, destroy app)

        (* main window layout *)
        val () = Box.setHomogeneous hBox false
        val () = Box.packStart hBox (cmdLbl,   false, false, 0)
        val () = Box.packStart hBox (cmdEntry, false, false, 0)
        val () = Box.packStart hBox (execBtn,  false, false, 0)
        val () = Box.packStart hBox (killBtn,  false, false, 0)
        val () = Box.packStart hBox (fontBtn,  true,  false, 0)
        local
          val spinBox = Box.new (Orientation.HORIZONTAL, 0)
        in
          val () = Box.setHomogeneous spinBox false
          val () = Box.packStart spinBox (spinLbl, false, false, 0)
          val () = Box.packStart spinBox (spinBtn, false, false, 0)
          val () = Box.packStart hBox (spinBox, true, false, 0)
        end
        val () = Box.packEnd   hBox (quitBtn,  false, false, 0)

        val () = Container.add scrWnd vte
        local
          val v = ValueAccessor.new int ~1
          val () = Widget.styleGetProperty scrWnd ("scrollbar-spacing", v)
          val spacing = ValueAccessor.get int v
        in
          val () = Container.setBorderWidth scrWnd spacing
        end
        val () = ScrolledWindow.setPolicy scrWnd (PolicyType.NEVER, PolicyType.ALWAYS)

        val () = Box.setHomogeneous vBox false
        val () = Box.packStart vBox (hBox,   false, false, 0)
        val () = Box.packEnd   vBox (scrWnd, true,  true,  0)

        val () = Container.add mainWnd vBox

        val () = Window.setTitle mainWnd "VteApp"
        val () = Window.setDefaultSize mainWnd (800, 450)

        (* Set up control widgets *)
        (*   - set actions *)
        local
          fun setButtonActionName (button, actionName) =
            Actionable.setActionName (Button.asActionable button) actionName
        in
          val () =
            List.app setButtonActionName [
              (execBtn, SOME "win.exec"),
              (killBtn, SOME "win.kill"),
              (fontBtn, SOME "win.font"),
              (quitBtn, SOME "app.quit")
            ]
        end

        (*   - set accelerators *)
        val () = addAccels app

        (*   - add actions to window *)
        val () =
          List.app (addSimpleAction (ApplicationWindow.asActionMap mainWnd)) [
            ("exec", SOME (cmdExec mainWnd widgets)),
            ("kill", SOME cmdKill),
            ("font", SOME (cmdFont mainWnd widgets))
          ]

        (*   - add actions to application *)
        val () =
          List.app (addSimpleAction (Application.asActionMap app)) [
            ("quit", SOME (cmdQuit mainWnd))
          ]

        (*   - set focus/sensitivity, must be done after actions *)
        val () = setWidgetProps widgets false

        (*   - set window default command and activate on enter in `cmdEntry` *)
        val () = Widget.setCanDefault execBtn true
        val () = Widget.grabDefault execBtn
        val () = Entry.setActivatesDefault cmdEntry true

        (*   - set default number of lines in scroll history *)
        val () = SpinButton.setValue spinBtn 10.0

        (*   - virtual terminal *)
        val () =
          VteTerminal.setColors vte (
            parseCheckColor "black",
            parseCheckColor "lightblue",
            GdkColorRecordCArrayN.fromList []
          )
        val _ = Signal.connect vte (VteTerminal.childExitedSig, childClose widgets)
        local
          fun setVteScrollback () =
            VteTerminal.setScrollbackLines vte (SpinButton.getValueAsInt spinBtn)
        in
          (* set scrollback lines now... *)
          val () = setVteScrollback ()

          (* ...and when changed.        *)
          val _ = Signal.connect spinBtn (SpinButton.valueChangedSig, setVteScrollback)
        end

        (* show everything *)
        val () = Widget.showAll mainWnd
      in
        mainWnd
      end

  end

mlton-main.sml

val () = main ()

mlton.mlb

local
  $(SML_LIB)/basis/basis.mlb
  $(GIRAFFE_SML_LIB)/general/mlton.mlb
  $(GIRAFFE_SML_LIB)/glib-2.0/mlton.mlb
  $(GIRAFFE_SML_LIB)/gobject-2.0/mlton.mlb
  $(GIRAFFE_SML_LIB)/gio-2.0/mlton.mlb
  $(GIRAFFE_SML_LIB)/gdk-3.0/mlton.mlb
  $(GIRAFFE_SML_LIB)/gtk-3.0/mlton.mlb
  $(GIRAFFE_SML_LIB)/vte-2.90/mlton.mlb
in
  VteMainWindow.sml
  vteapp.sml
  mlton-main.sml
end

polyml-libs.sml

use "$(GIRAFFE_SML_LIB)/general/polyml.sml";
use "$(GIRAFFE_SML_LIB)/ffi/polyml.sml";
use "$(GIRAFFE_SML_LIB)/gir/polyml.sml";
use "$(GIRAFFE_SML_LIB)/glib-2.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/gobject-2.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/gio-2.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/cairo-1.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/pango-1.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/pangocairo-1.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/gdkpixbuf-2.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/atk-1.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/gdk-3.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/xlib-2.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/gtk-3.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/vte-2.90/polyml.sml";

polyml-app.sml

(* For each line of the form
 *
 *   use "<file>";
 *
 * <file> is taken as a build dependency.
 *)

use "VteMainWindow.sml";
use "vteapp.sml";

app.mk

################################################################################
# Application-specific values

NAME := vteapp


# MLton target
#
# Define:
#   SRC_MLTON       - the SML source files for MLton
#   TARGET_MLTON    - the binary to be built with MLton

ifdef MLTON_VERSION

SRC_MLTON := $(shell $(MLTON_MLTON) -mlb-path-var 'GIRAFFE_SML_LIB $(GIRAFFE_SML_LIBDIR)' -stop f mlton.mlb)

TARGET_MLTON := $(NAME)-mlton

endif


# Poly/ML target
#
# Define:
#   SRC_POLYML      - the SML source files for Poly/ML
#   TARGET_POLYML   - the binary to be built with Poly/ML

ifdef POLYML_VERSION

SRC_POLYML := $(shell cat polyml-app.sml | sed -n 's|^use "\([^"]*\)";$$|\1|p')

TARGET_POLYML := $(NAME)-polyml

endif


# Library dependencies
#
# Define:
#   LIB_NAMES       - list of the libraries that the application references

LIB_NAMES := \
	glib-2.0 \
	gobject-2.0 \
	gio-2.0 \
	gdk-3.0 \
	gtk-3.0 \
	vte-2.90

# Note that LIB_NAMES does _not_ contain pkg-config names but GIR namespace
# names, which are also the directory names in $(GIRAFFEHOME)/lib/sml.
#
# One method to determine the list is as follows: for each instance of
#
#   $(GIRAFFE_SML_LIB)/$(LIB_NAME)/mlton.mlb
#
# in mlton.mlb, the list should include $(LIB_NAME).