How to execute shell command in Ide.TerminalPage from an addin?

I’m making an addin to run current file based on it’s extension. I need a terminal to communicate with launched program. I create Ide.TerminalPage at construction time and, after project is loaded, add TerminalPage to bottom area. I need terminal widget to not show anything before user launches the program. But TerminalPage starts with host interactive shell.
To run program RunCommand and TerminalLauncher are created. Calling spawn_async somewhat works, but program output “layers” on top of interactive shell.

Could you provide some basic steps on how to create TerminalPage and run program in it? I tried looking at Builder’s source, but it’s a bit overwhelming.

Here is my typescript code:

import GObject from 'gi://GObject';
import Gtk from 'gi://Gtk?version=4.0';
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import Ide from 'gi://Ide';
import Panel from 'gi://Panel';

var c_files = ['c', 'cpp'];

export var CodeRunAddin = GObject.registerClass({
    Implements: [Ide.WorkspaceAddin],
}, class CodeRunAddin extends GObject.Object {

    workspace?: Ide.Workspace = undefined;
    run_button: Gtk.Button;
    context: Ide.Context|null = null;
    podman_runtime?: Ide.Runtime = undefined;
    source: Gio.File|null = null;
    term: Ide.TerminalPage;

    constructor() {
        super();

        this.term = new Ide.TerminalPage({
            pty: Ide.pty_new_sync(),
            respawnOnExit: false,
            manageSpawn: true,
        });

        this.run_button = new Gtk.Button({
            visible: false,
            iconName: "media-seek-forward-symbolic",
        });
        this.run_button.connect("clicked", this.compile.bind(this));
    }

    compile () {
        // Ide.Run
        var builder = new Ide.RunContext();
        var runner = new Ide.RunContext();
        if (this.podman_runtime !== undefined && this.source?.get_path() !== null) {
            var cwd = this.source?.get_parent()!;
            var source_rel = cwd.get_relative_path(this.source!)!;
            
            this.podman_runtime.prepare_to_build(null, builder);
            builder.set_cwd(cwd.get_path()!);
            print(`Compiler cwd: \"${builder.get_cwd()}\"`);
            // runner.append_argv("lsb-release");
            // runner.append_argv("-i");
            builder.append_argv("g++");
            builder.append_argv(source_rel);
            builder.append_argv("-o");
            builder.append_argv("main");

            try {
                var process = builder.spawn();
                process.wait(null);
            } catch (e: any) {
                print(e.message);
            }

            this.podman_runtime.prepare_to_run(null, runner);
            runner.set_cwd(cwd.get_path()!);
            print(`Runner cwd: \"${runner.get_cwd()}\"`);
            // runner.append_argv("lsb-release");
            // runner.append_argv("-i");
            runner.append_argv("./main");

            try {
                var command = new Ide.RunCommand();
                var launcher = Ide.TerminalLauncher.new(this.context!, command);

                command.prepare_to_run(runner, this.context!);
                command.set_cwd(cwd.get_path()!);
                command.append_argv("./main");
                
                print("term pty:", this.term.get_pty());
                launcher.spawn_async(this.term.get_pty(), null, null);
            } catch (e: any) {
                print(e.message);
            }
        }
    }

    vfunc_load(_workspace: Ide.Workspace) {
        this.workspace = _workspace;
        if (this.workspace.context?.has_project() == false) {
            this.vfunc_unload(_workspace);
            return;
        } else {
            print("Project seems to be loaded");
        }

        var pane = new Ide.Pane();
        pane.set_child(this.term);
        var pos = new Panel.Position();
        pos.set_area(Panel.Area.BOTTOM);
        _workspace.add_pane(pane, pos);
        var titlebar = this.workspace?.get_header_bar();
        titlebar?.add(Ide.HeaderBarPosition.RIGHT_OF_CENTER, 1, this.run_button);
        this.context = _workspace.get_context();
        var runtime_manager = Ide.RuntimeManager.from_context(this.context!);

        var fd = runtime_manager.connect("items-changed", this.on_runtimes_changed.bind(this));

        print("fd = ", fd);

        // for (var i: number = 0; i < runtime_manager.get_n_items(); i += 1) {
        //     var item: Ide.Runtime = runtime_manager.get_item(i)!;
        //     print(item.name);
        // }

        print("Workspace addin loaded");
    }

    on_runtimes_changed(self: Gio.ListModel, position: number, removed: number, added: number) {
        for (var i = position; i < position + added; i+=1) {
            var item: Ide.Runtime = self.get_item(i)!;
            if (item.id?.startsWith("podman:")) {
                print (`New runtime appears: \"${item.name}\"`);
                if (item.name == "dbx-arch") {
                    print("Found desired runtime");
                    this.podman_runtime = item;
                }
            }
        }
    }

    vfunc_unload(workspace: Ide.Workspace) {
        print("Workspace addin unloaded");
    }

    vfunc_page_changed(some_page: Ide.Page) {
        if (some_page.constructor.$gtype === Ide.EditorPage.$gtype) {
            print("Editor page");
            var editor_page: Ide.EditorPage = some_page as Ide.EditorPage;
            var source_file: Gio.File = editor_page.get_file();
            var extension = source_file.get_path()?.split('.').pop();

            print("Current extension:", extension);
            if (extension !== undefined && c_files.includes(extension)) {
                this.run_button.set_visible(true);
                this.source = source_file;
            } else {
                print("Unsupported extension");
                this.run_button.set_visible(false);
            }
        } else {
            this.run_button.set_visible(false);
        }
    }
});

If you want the Terminal page to be persistent and used across multiple runs of the application you probably want to do this a little backwards from what other terminal usage does. It would likely need to work more like how the Build Output connects to the Build Pipeline.

By that, I mean, create an Ide.TerminalPage and set manageSpawn to false. Then, executing your run command, connect the Pty from the Terminal in the Ide.RunContext as the controlling TTY. The Ide.RunContext.set_pty() helper can do that for you.

To avoid issues though, you’ll need to make sure only one runs at a time.

1 Like

@chergert Could you also help me with embedding gresource?
I found this page: Creating Your First Extension — Builder 45.alpha documentation. I followed this guide and now I have this file structure:

⬢ CodeRun ls
code_run.gresource  code_run.plugin  main.js
⬢ CodeRun gresource list code_run.gresource
/plugins/code_run/code_run_pane.ui
⬢ CodeRun pwd           
/home/dell/.local/share/gnome-builder/plugins/CodeRun

ui file is referenced like this:

var CodeRunPane = GObject.registerClass({
    Template: "resource:///plugins/code_run/code_run_pane.ui",
    Children: ["build_term", "run_term"],
    GTypeName: 'CodeRunPane',
},

But when running Builder I get following errors:

 ~ LANG="en_US.UTF-8" flatpak run org.gnome.Builder
Gjs-Console-Message: 14:58:18.511: /home/dell

(gnome-builder:2): Gtk-CRITICAL **: 14:58:18.512: Unable to load resource for composite template for type 'CodeRunPane': The resource at “/plugins/code_run/code_run_pane.ui” does not exist
14:58:18.5127                                       Gtk[    2]: CRITICAL: gtk_widget_class_set_template_scope: assertion 'widget_class->priv->template != NULL' failed
14:58:18.5127                                       Gtk[    2]: CRITICAL: gtk_widget_class_bind_template_child_full: assertion 'widget_class->priv->template != NULL' failed
14:58:18.5127                                       Gtk[    2]: CRITICAL: gtk_widget_class_bind_template_child_full: assertion 'widget_class->priv->template != NULL' failed
14:58:18.5514                                       Gtk[    2]: CRITICAL: gtk_widget_init_template: assertion 'template != NULL' failed
14:58:18.5516                              GLib-GObject[    2]: CRITICAL: Custom constructor for class Gjs_CodeRunAddin returned NULL (which is invalid). Please use GInitable instead.
14:58:18.5516                                   libpeas[    2]:  WARNING: Plugin 'main' does not provide a 'IdeWorkspaceAddin' extension

All sources can be found here: RocketRide / Builder Code Run · GitLab

I’d first look to see if your resources have been loaded into the process. The GTK inspector has a resource browser you can use to verify.

If not, the next step is to figure out where those should get loaded from and get them loaded. Since we mostly do in-process plug-ins, I’m not sure we have anything auto-loading the .gresource file for you.

1 Like

Docs says that Builder automatically loads gresources for you:

Sometimes plugins need to embed resources. Builder will automatically load a file that matches the name $module_name.gresource if it placed alongside the $module_name.plugin file.

Is it no longer the case?

It looks like you need the Has-Resources key in the .plugin file:

1 Like

Thanks. After adding X-Has-Resources=true i can see resources loaded in Inspector, but my plugin still can’t find resources.

 ~ LANG="en_US.UTF-8" flatpak run org.gnome.Builder

(gnome-builder:2): Gtk-CRITICAL **: 15:25:46.536: Unable to load resource for composite template for type 'CodeRunControlButton': The resource at “/plugins/code_run/code_run_control_button.ui” does not exist
15:25:46.5369                                       Gtk[    2]: CRITICAL: gtk_widget_class_set_template_scope: assertion 'widget_class->priv->template != NULL' failed
15:25:46.5370                                       Gtk[    2]: CRITICAL: gtk_widget_class_bind_template_child_full: assertion 'widget_class->priv->template != NULL' failed
15:25:46.5370                                       Gtk[    2]: CRITICAL: gtk_widget_class_bind_template_child_full: assertion 'widget_class->priv->template != NULL' failed
15:25:46.5370                                       Gtk[    2]: CRITICAL: gtk_widget_class_bind_template_child_full: assertion 'widget_class->priv->template != NULL' failed

(gnome-builder:2): Gtk-CRITICAL **: 15:25:46.537: Unable to load resource for composite template for type 'CodeRunPane': The resource at “/plugins/code_run/code_run_pane.ui” does not exist
15:25:46.5380                                       Gtk[    2]: CRITICAL: gtk_widget_class_set_template_scope: assertion 'widget_class->priv->template != NULL' failed
15:25:46.5380                                       Gtk[    2]: CRITICAL: gtk_widget_class_bind_template_child_full: assertion 'widget_class->priv->template != NULL' failed
15:25:46.5380                                       Gtk[    2]: CRITICAL: gtk_widget_class_bind_template_child_full: assertion 'widget_class->priv->template != NULL' failed
15:25:46.5380                                       Gtk[    2]: CRITICAL: gtk_widget_class_bind_template_child_full: assertion 'widget_class->priv->template != NULL' failed
15:25:46.5777                                       Gtk[    2]: CRITICAL: gtk_widget_init_template: assertion 'template != NULL' failed
15:25:46.5781                                       Gtk[    2]: CRITICAL: gtk_widget_init_template: assertion 'template != NULL' failed
15:25:46.5782                              GLib-GObject[    2]: CRITICAL: Custom constructor for class Gjs_CodeRunAddin returned NULL (which is invalid). Please use GInitable instead.
15:25:46.5782                                   libpeas[    2]:  WARNING: Plugin 'code_run' does not provide a 'IdeWorkspaceAddin' extension

One possible problem here that may need to be addressed in Builder itself will have to do with when the resources are loaded. Perhaps GJS is trying to parse/load your class_init before the resources are registered.