diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d06b8a1439..9a1c7fda87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: - name: Install Dependencies run: | apt update - apt install -y exuberant-ctags libeditorconfig-dev libgail-3-dev libgee-0.8-dev libgit2-glib-1.0-dev libgranite-dev libgtk-3-dev libgtksourceview-4-dev libgtkspell3-3-dev libhandy-1-dev libpeas-dev libsoup2.4-dev libvala-dev libvte-2.91-dev meson valac + apt install -y exuberant-ctags libeditorconfig-dev libgail-3-dev libgee-0.8-dev libgit2-glib-1.0-dev libgranite-dev libgtk-3-dev libgtksourceview-4-dev libgtkspell3-3-dev libhandy-1-dev libjson-glib-dev libpeas-dev libsoup2.4-dev libvala-dev libvte-2.91-dev meson valac - name: Build env: DESTDIR: out diff --git a/README.md b/README.md index 9f440e8b3e..5624828e4a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ You'll need the following dependencies: * libgtkspell3-3-dev * libgranite-dev >= 6.0.0 * libhandy-1-dev >= 0.90.0 +* libjson-glib-dev * libpeas-dev * libsoup2.4-dev * libvala-0.48-dev (or higher) diff --git a/flatpak/fuse-2.9.2-namespace-conflict-fix.patch b/flatpak/fuse-2.9.2-namespace-conflict-fix.patch new file mode 100644 index 0000000000..ae67e7d45b --- /dev/null +++ b/flatpak/fuse-2.9.2-namespace-conflict-fix.patch @@ -0,0 +1,21 @@ +diff -up fuse-2.9.2/include/fuse_kernel.h.conflictfix fuse-2.9.2/include/fuse_kernel.h +--- fuse-2.9.2/include/fuse_kernel.h.conflictfix 2013-06-26 09:31:57.862198038 -0400 ++++ fuse-2.9.2/include/fuse_kernel.h 2013-06-26 09:32:19.679198365 -0400 +@@ -88,12 +88,16 @@ + #ifndef _LINUX_FUSE_H + #define _LINUX_FUSE_H + +-#include ++#ifdef __linux__ ++#include ++#else ++#include + #define __u64 uint64_t + #define __s64 int64_t + #define __u32 uint32_t + #define __s32 int32_t + #define __u16 uint16_t ++#endif + + /* + * Version negotiation: diff --git a/flatpak/fuse-disable-sys-mount-under-flatpak.patch b/flatpak/fuse-disable-sys-mount-under-flatpak.patch new file mode 100644 index 0000000000..fb5ba22860 --- /dev/null +++ b/flatpak/fuse-disable-sys-mount-under-flatpak.patch @@ -0,0 +1,25 @@ +From 1ec935f4abecd08957affc7b21bae6bf5be78931 Mon Sep 17 00:00:00 2001 +From: Christian Hergert +Date: Thu, 12 Apr 2018 01:47:57 -0700 +Subject: [PATCH] libfuse: disable sys mount under flatpak + +--- + lib/mount.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/lib/mount.c b/lib/mount.c +index 7a18c11..1667db2 100644 +--- a/lib/mount.c ++++ b/lib/mount.c +@@ -392,6 +392,9 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo, + int fd; + int res; + ++ /* disable in flatpak */ ++ return -2; ++ + if (!mnt) { + fprintf(stderr, "fuse: missing mountpoint parameter\n"); + return -1; +-- +2.17.0.rc2 diff --git a/flatpak/fusermount-wrapper.sh b/flatpak/fusermount-wrapper.sh new file mode 100755 index 0000000000..24bfa9dac7 --- /dev/null +++ b/flatpak/fusermount-wrapper.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +if [ -z "$_FUSE_COMMFD" ]; then + FD_ARGS= +else + FD_ARGS="--env=_FUSE_COMMFD=${_FUSE_COMMFD} --forward-fd=${_FUSE_COMMFD}" +fi + +exec flatpak-spawn --host --forward-fd=1 --forward-fd=2 --forward-fd=3 $FD_ARGS fusermount "$@" diff --git a/io.elementary.code.yml b/io.elementary.code.yml index 59db37b3af..d72790271d 100644 --- a/io.elementary.code.yml +++ b/io.elementary.code.yml @@ -4,23 +4,36 @@ runtime-version: '6' sdk: io.elementary.Sdk command: io.elementary.code finish-args: + - '--require-version=1.0.0' - '--filesystem=host' + # flatpak access + - '--filesystem=/var/lib/flatpak' + - '--filesystem=~/.local/share/flatpak' + - '--share=ipc' + - '--share=network' - '--socket=fallback-x11' - '--socket=wayland' + - '--allow=devel' - '--talk-name=org.gtk.vfs.*' - '--talk-name=org.gnome.SettingsDaemon' - '--talk-name=org.elementary.Contractor' + - '--talk-name=org.freedesktop.Flatpak' - '--metadata=X-DConf=migrate-path=/io/elementary/code/' +build-options: + env: + MOUNT_FUSE_PATH: ../tmp/ + V: '1' cleanup: - '/include' - '/lib/pkgconfig' - '/lib/cmake' - '/lib/girepository-1.0' - '/share/gir-1.0' + - '/share/man' - '/share/vala' - '*.a' - '*.la' @@ -61,7 +74,6 @@ modules: - '-DBUILD_TESTING:BOOL=OFF' - '-DCMAKE_INSTALL_LIBDIR:PATH=/app/lib' cleanup: - - '/share/man' - '/share/doc' sources: - type: git @@ -119,6 +131,87 @@ modules: url: https://github.com/universal-ctags/ctags.git tag: p5.9.20201101.0 + - name: flatpak + config-opts: + - '--disable-documentation' + - '--disable-seccomp' + - '--disable-sandboxed-triggers' + - '--disable-system-helper' + - '--with-system-install-dir=/var/lib/flatpak' + - '--sysconfdir=/var/run/host/etc' + cleanup: + - '/bin/flatpak-bisect' + - '/bin/flatpak-coredumpctl' + - '/etc/profile.d' + - '/libexec' + - '/lib/systemd' + - '/share/dbus-1/interfaces/org.freedesktop.*' + - '/share/dbus-1/services/org.freedesktop.*' + - '/share/fish' + - '/share/flatpak/triggers' + - '/share/gdm' + - '/share/zsh' + post-install: + - 'cp /usr/bin/update-mime-database /app/bin' + - 'cp /usr/bin/update-desktop-database /app/bin' + sources: + - type: git + url: https://github.com/flatpak/flatpak + tag: '1.11.2' + modules: + - name: libfuse + config-opts: + - 'UDEV_RULES_PATH=/app/etc/udev/rules.d' + - 'INIT_D_PATH=/app/etc/init.d' + cleanup: + - '/bin/ulockmgr_server' + post-install: + - 'install -m a+rx fusermount-wrapper.sh /app/bin/fusermount' + sources: + - type: archive + url: https://github.com/libfuse/libfuse/releases/download/fuse-2.9.9/fuse-2.9.9.tar.gz + sha256: d0e69d5d608cc22ff4843791ad097f554dd32540ddc9bed7638cc6fea7c1b4b5 + - type: patch + path: flatpak/fuse-2.9.2-namespace-conflict-fix.patch + - type: patch + path: flatpak/fuse-disable-sys-mount-under-flatpak.patch + - type: file + path: flatpak/fusermount-wrapper.sh + - name: ostree + config-opts: + - '--disable-man' + - '--without-libsystemd' + cleanup: + - /bin + - /etc/grub.d + - /etc/ostree + - /libexec + - /share/ostree + sources: + - type: git + url: https://github.com/ostreedev/ostree + branch: v2021.1 + - name: pyparsing + buildsystem: simple + build-commands: + - 'pip3 install --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} pyparsing' + sources: + - type: file + url: https://files.pythonhosted.org/packages/b9/b8/6b32b3e84014148dcd60dd05795e35c2e7f4b72f918616c61fdce83d27fc/pyparsing-2.3.1.tar.gz + sha256: 66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a + + - name: flatpak-builder + sources: + - type: git + url: https://github.com/flatpak/flatpak-builder + tag: '1.0.14' + modules: + - name: yaml + sources: + - type: git + url: https://github.com/yaml/libyaml + tag: '0.2.5' + - name: code buildsystem: meson config-opts: diff --git a/meson.build b/meson.build index f624507a65..53ead360a6 100644 --- a/meson.build +++ b/meson.build @@ -31,6 +31,7 @@ gee_dep = dependency('gee-0.8', version: '>=0.8.5') gtk_dep = dependency('gtk+-3.0', version: '>=3.6.0') granite_dep = dependency('granite', version: '>=6.0.0') handy_dep = dependency('libhandy-1', version: '>=0.90.0') +json_dep = dependency('json-glib-1.0') gtksourceview_dep = dependency('gtksourceview-4') peas_dep = dependency('libpeas-1.0') peasgtk_dep = dependency('libpeas-gtk-1.0') @@ -51,6 +52,7 @@ dependencies = [ gtk_dep, granite_dep, handy_dep, + json_dep, gtksourceview_dep, peas_dep, peasgtk_dep, diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index f86f5a5069..151f55f657 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -26,6 +26,7 @@ namespace Scratch.FolderManager { private GLib.Settings settings; public signal void select (string file); + public signal void select_project (string path); public signal void close_all_docs_from_path (string path); // This is a workaround for SourceList silliness: you cannot remove an item @@ -53,7 +54,18 @@ namespace Scratch.FolderManager { } if (item is FileItem) { - select (((FileItem) item).file.path); + var file_item = (FileItem) item; + var project_path = file_item.file.path; + + select (project_path); + + var item_for_path = (Item?)(expand_to_path (project_path)); + if (item_for_path != null) { + var search_root = item_for_path.get_root_folder (); + if (search_root is ProjectFolderItem) { + select_project (search_root.file.file.get_path ()); + } + } } } @@ -73,6 +85,8 @@ namespace Scratch.FolderManager { return; } + select_project (folder.path); + add_folder (folder, true); } diff --git a/src/MainWindow.vala b/src/MainWindow.vala index bb7f3142c3..9c13564a27 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -24,6 +24,9 @@ namespace Scratch { public const int FONT_SIZE_MIN = 7; private const uint MAX_SEARCH_TEXT_LENGTH = 255; + private Services.ProjectManager project_manager; + private Gee.ArrayList project_output; + public weak Scratch.Application app { get; construct; } public Scratch.Widgets.DocumentView document_view; @@ -34,6 +37,8 @@ namespace Scratch { public Scratch.Widgets.SearchBar search_bar; private Code.WelcomeView welcome_view; private FolderManager.FileView folder_manager_view; + private Gtk.Revealer terminal_revealer; + private Scratch.Widgets.Terminal terminal; // Plugins private Scratch.Services.PluginsManager plugins; @@ -55,6 +60,7 @@ namespace Scratch { public SimpleActionGroup actions { get; construct; } public const string ACTION_PREFIX = "win."; + public const string ACTION_BUILD = "action_build"; public const string ACTION_FIND = "action_find"; public const string ACTION_FIND_NEXT = "action_find_next"; public const string ACTION_FIND_PREVIOUS = "action_find_previous"; @@ -70,10 +76,12 @@ namespace Scratch { public const string ACTION_PREFERENCES = "preferences"; public const string ACTION_UNDO = "action_undo"; public const string ACTION_REDO = "action_redo"; + public const string ACTION_RUN = "action_run"; public const string ACTION_REVERT = "action_revert"; public const string ACTION_SAVE = "action_save"; public const string ACTION_SAVE_AS = "action_save_as"; public const string ACTION_SHOW_FIND = "action_show_find"; + public const string ACTION_STOP = "action_stop"; public const string ACTION_TEMPLATES = "action_templates"; public const string ACTION_SHOW_REPLACE = "action_show_replace"; public const string ACTION_TO_LOWER_CASE = "action_to_lower_case"; @@ -94,6 +102,7 @@ namespace Scratch { public static Gee.MultiMap action_accelerators = new Gee.HashMultiMap (); private const ActionEntry[] ACTION_ENTRIES = { + { ACTION_BUILD, action_build }, { ACTION_FIND, action_fetch, "s" }, { ACTION_FIND_NEXT, action_find_next }, { ACTION_FIND_PREVIOUS, action_find_previous }, @@ -103,10 +112,12 @@ namespace Scratch { { ACTION_COLLAPSE_ALL_FOLDERS, action_collapse_all_folders }, { ACTION_ORDER_FOLDERS, action_order_folders }, { ACTION_PREFERENCES, action_preferences }, + { ACTION_RUN, action_run }, { ACTION_REVERT, action_revert }, { ACTION_SAVE, action_save }, { ACTION_SAVE_AS, action_save_as }, { ACTION_SHOW_FIND, action_show_fetch, null, "false" }, + { ACTION_STOP, action_stop }, { ACTION_TEMPLATES, action_templates }, { ACTION_GO_TO, action_go_to }, { ACTION_SORT_LINES, action_sort_lines }, @@ -142,11 +153,13 @@ namespace Scratch { } static construct { + action_accelerators.set (ACTION_BUILD, "F4"); action_accelerators.set (ACTION_FIND + "::", "f"); action_accelerators.set (ACTION_FIND_NEXT, "g"); action_accelerators.set (ACTION_FIND_PREVIOUS, "g"); action_accelerators.set (ACTION_FIND_GLOBAL + "::", "f"); action_accelerators.set (ACTION_OPEN, "o"); + action_accelerators.set (ACTION_RUN, "F5"); action_accelerators.set (ACTION_REVERT, "o"); action_accelerators.set (ACTION_SAVE, "s"); action_accelerators.set (ACTION_SAVE_AS, "s"); @@ -156,6 +169,7 @@ namespace Scratch { action_accelerators.set (ACTION_UNDO, "z"); action_accelerators.set (ACTION_REDO, "z"); action_accelerators.set (ACTION_SHOW_REPLACE, "r"); + action_accelerators.set (ACTION_STOP, "F6"); action_accelerators.set (ACTION_TO_LOWER_CASE, "l"); action_accelerators.set (ACTION_TO_UPPER_CASE, "u"); action_accelerators.set (ACTION_DUPLICATE, "d"); @@ -260,6 +274,32 @@ namespace Scratch { // Show/Hide widgets show_all (); + project_manager = Services.ProjectManager.get_instance (); + project_output = new Gee.ArrayList (); + + project_manager.on_standard_output.connect ((line) => { + project_output.add (line); + terminal.buffer.text += line; + terminal.attempt_scroll (); + }); + + project_manager.on_standard_error.connect ((line) => { + project_output.add (line); + terminal.buffer.text += line; + terminal.attempt_scroll (); + }); + + project_manager.on_clear.connect ((line) => { + project_output.clear (); + terminal.buffer.text = ""; + terminal.attempt_scroll (); + }); + + set_build_run_widgets_sensitive (); + project_manager.notify.connect ((s, p) => { + set_build_run_widgets_sensitive (); + }); + toolbar.templates_button.visible = (plugins.plugin_iface.template_manager.template_available); plugins.plugin_iface.template_manager.notify["template_available"].connect (() => { toolbar.templates_button.visible = (plugins.plugin_iface.template_manager.template_available); @@ -335,6 +375,11 @@ namespace Scratch { } }); + + folder_manager_view.select_project.connect ((path) => { + project_manager.project_path = path; + }); + folder_manager_view.close_all_docs_from_path.connect ((a) => { var docs = document_view.docs.copy (); docs.foreach ((doc) => { @@ -353,6 +398,11 @@ namespace Scratch { on_plugin_toggled (bottombar); }); + // Terminal + terminal = new Scratch.Widgets.Terminal (new Gtk.TextBuffer (null)); + terminal_revealer = new Gtk.Revealer (); + terminal_revealer.add (terminal); + var view_grid = new Gtk.Grid () { orientation = Gtk.Orientation.VERTICAL }; @@ -368,13 +418,17 @@ namespace Scratch { content_stack.add (welcome_view); content_stack.visible_child = view_grid; // Must be visible while restoring + var content_paned = new Gtk.Paned (Gtk.Orientation.VERTICAL); + content_paned.add (content_stack); + content_paned.add (terminal_revealer); + // Set a proper position for ThinPaned widgets int width, height; get_size (out width, out height); vp = new Gtk.Paned (Gtk.Orientation.VERTICAL); vp.position = (height - 150); - vp.pack1 (content_stack, true, false); + vp.pack1 (content_paned, true, false); vp.pack2 (bottombar, false, false); hp1 = new Gtk.Paned (Gtk.Orientation.HORIZONTAL); @@ -389,6 +443,8 @@ namespace Scratch { add (grid); search_revealer.set_reveal_child (false); + terminal_revealer.set_reveal_child (false); + terminal_revealer.visible = false; realize.connect (() => { Scratch.saved_state.bind ("sidebar-visible", sidebar, "visible", SettingsBindFlags.DEFAULT); @@ -544,6 +600,30 @@ namespace Scratch { } } + private void set_build_run_widgets_sensitive () { + bool has_dependencies_installed = project_manager.has_dependencies_installed; + bool is_project_selected = (project_manager.project_path != null) && (project_manager.get_flatpak_manifest () != null); + bool is_running = project_manager.is_running; + + Utils.action_from_group (ACTION_BUILD, actions).set_enabled (has_dependencies_installed && is_project_selected && !is_running); + Utils.action_from_group (ACTION_RUN, actions).set_enabled (has_dependencies_installed && is_project_selected && !is_running); + Utils.action_from_group (ACTION_STOP, actions).set_enabled (has_dependencies_installed && is_project_selected && is_running); + + if (is_project_selected) { + var project_name = Path.get_basename (project_manager.project_path); + toolbar.build_button.tooltip_markup = _("Build ”%s”".printf (project_name)); + toolbar.run_button.tooltip_markup = _("Run ”%s”".printf (project_name)); + toolbar.stop_button.tooltip_markup = _("Stop ”%s”".printf (project_name)); + } else { + toolbar.build_button.tooltip_markup = _("Build"); + toolbar.run_button.tooltip_markup = _("Run"); + toolbar.stop_button.tooltip_markup = _("Stop"); + } + + terminal_revealer.set_reveal_child (is_running); + terminal_revealer.visible = is_running; + } + // Get current document public Scratch.Services.Document? get_current_document () { return document_view.current_document; @@ -637,6 +717,7 @@ namespace Scratch { private void handle_quit () { document_view.save_opened_files (); update_saved_state (); + action_stop (); } public void set_default_zoom () { @@ -822,6 +903,28 @@ namespace Scratch { } } + private void action_build () { + project_manager.build.begin ((obj, res) => { + var success = project_manager.build.end (res); + if (!success && !project_manager.was_stopped) { + show_error_dialog (); + } + }); + } + + private void action_run () { + project_manager.build_install_run.begin ((obj, res) => { + var success = project_manager.build_install_run.end (res); + if (!success && !project_manager.was_stopped) { + show_error_dialog (); + } + }); + } + + private void action_stop () { + project_manager.stop (); + } + private void action_revert () { var confirmation_dialog = new Scratch.Dialogs.RestoreConfirmationDialog (this); if (confirmation_dialog.run () == Gtk.ResponseType.ACCEPT) { @@ -1024,6 +1127,27 @@ namespace Scratch { } return path; - } + } + + private void show_error_dialog () { + var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( + _("Project “%s“ could not be built").printf (project_manager.project_name), + "", + "media-playback-start" + ); + message_dialog.badge_icon = new ThemedIcon ("dialog-error"); + message_dialog.transient_for = this; + + if (project_output.size > 0) { + message_dialog.secondary_text = project_output.get (project_output.size - 1); + } + + message_dialog.show_error_details (string.joinv ("\n", project_output.to_array ())); + + message_dialog.show_all (); + message_dialog.response.connect ((response_id) => { + message_dialog.destroy (); + }); + } } } diff --git a/src/Services/ProjectManager.vala b/src/Services/ProjectManager.vala new file mode 100644 index 0000000000..e6a83b3877 --- /dev/null +++ b/src/Services/ProjectManager.vala @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2021 elementary, Inc. (https://elementary.io) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * Authored by: Marius Meisenzahl + */ + +public class Scratch.Services.ProjectManager : Object { + public string project_path { get; set; } + public string project_name { + owned get { + return Path.get_basename (project_path); + } + } + public bool is_running { get; set; } + public bool was_stopped { get; set; } + + public bool is_running_flatpaked { + get { + return FileUtils.test ("/.flatpak-info", FileTest.IS_REGULAR); + } + } + + public bool has_dependencies_installed { + get { + if (is_running_flatpaked) { + return run_command_sync ({ + "flatpak-spawn", + "--host", + "flatpak-builder", + "--version" + }); + } else { + return run_command_sync ({ + "flatpak-builder", + "--version" + }); + } + } + } + + public signal void on_standard_output (string line); + public signal void on_standard_error (string line); + public signal void on_clear (); + + static ProjectManager? instance; + + private Pid command_pid; + + public static ProjectManager get_instance () { + if (instance == null) { + instance = new ProjectManager (); + } + + return instance; + } + + public FlatpakManifest? get_flatpak_manifest () { + Dir dir; + try { + dir = Dir.open (project_path, 0); + } catch (Error e) { + warning ("Could not read Flatpak manifest: %s", e.message); + return null; + } + + string? name = null; + while ((name = dir.read_name ()) != null) { + string f = Path.build_filename (project_path, name); + + if (FileUtils.test (f, FileTest.IS_REGULAR)) { + if (f.has_suffix (".yml") || f.has_suffix (".yaml")) { + try { + string content; + FileUtils.get_contents (f, out content); + + var re_app_id = new Regex ("app-id:\\s*(?P[A-Za-z0-9-\\.]+)"); + var re_command = new Regex ("command:\\s*(?P[A-Za-z0-9-\\.]+)"); + + var flatpak_manifest = new FlatpakManifest () { + manifest = f, + build_dir = Path.build_filename (project_path, "flatpak-build") + }; + + MatchInfo mi; + if (re_app_id.match (content, 0, out mi)) { + flatpak_manifest.app_id = mi.fetch_named ("app_id"); + } + + if (re_command.match (content, 0, out mi)) { + flatpak_manifest.command = mi.fetch_named ("command"); + } + + if (flatpak_manifest.app_id.length > 0 && flatpak_manifest.command.length > 0) { + return flatpak_manifest; + } + } catch (Error e) { + warning ("Could not read Flatpak manifest: %s", e.message); + } + } else if (f.has_suffix (".json")) { + try { + string content; + FileUtils.get_contents (f, out content); + + var parser = new Json.Parser (); + parser.load_from_data (content, -1); + var object = parser.get_root ().get_object (); + + return new FlatpakManifest () { + manifest = f, + build_dir = Path.build_filename (project_path, "flatpak-build"), + app_id = object.get_string_member ("app-id"), + command = object.get_string_member ("command") + }; + } catch (Error e) { + warning ("Could not read Flatpak manifest: %s", e.message); + } + } + } + } + + return null; + } + + public class FlatpakManifest : Object { + public string manifest { get; set; } + public string build_dir { get; set; } + public string app_id { get; set; } + public string command { get; set; } + } + + private bool process_line (IOChannel channel, IOCondition condition, string stream_name) { + if (condition == IOCondition.HUP) { + return false; + } + + try { + string line; + channel.read_line (out line, null, null); + + switch (stream_name) { + case "stdout": + print (line); + on_standard_output (line); + break; + case "stderr": + print (line); + on_standard_error (line); + break; + } + } catch (IOChannelError e) { + warning ("%s: IOChannelError: %s", stream_name, e.message); + return false; + } catch (ConvertError e) { + warning ("%s: ConvertError: %s", stream_name, e.message); + return false; + } + + return true; + } + + private async bool run_command_async (string[] cmd) { + MainLoop loop = new MainLoop (); + bool exit_status = false; + + try { + string[] spawn_args = cmd; + string[] spawn_env = Environ.get (); + + int standard_input; + int standard_output; + int standard_error; + + Process.spawn_async_with_pipes ( + project_path, + spawn_args, + spawn_env, + SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD, + null, + out command_pid, + out standard_input, + out standard_output, + out standard_error + ); + + // stdout + IOChannel output = new IOChannel.unix_new (standard_output); + output.add_watch (IOCondition.IN | IOCondition.HUP, (channel, condition) => { + return process_line (channel, condition, "stdout"); + }); + + // stderr + IOChannel error = new IOChannel.unix_new (standard_error); + error.add_watch (IOCondition.IN | IOCondition.HUP, (channel, condition) => { + return process_line (channel, condition, "stderr"); + }); + + ChildWatch.add (command_pid, (pid, status) => { + // Triggered when the child indicated by command_pid exits + Process.close_pid (pid); + exit_status = (status == 0); + loop.quit (); + }); + + loop.run (); + + return exit_status; + } catch (SpawnError e) { + warning ("Could not run command: %s\n", e.message); + return false; + } + } + + private bool run_command_sync (string[] cmd) { + try { + string[] spawn_args = cmd; + string[] spawn_env = Environ.get (); + + int status; + + Process.spawn_sync (project_path, + spawn_args, + spawn_env, + SpawnFlags.SEARCH_PATH | SpawnFlags.STDERR_TO_DEV_NULL | SpawnFlags.STDOUT_TO_DEV_NULL , + null, + null, + null, + out status); + + return status == 0; + } catch (SpawnError e) { + print ("Error: %s\n", e.message); + } + + return false; + } + + private async bool build_project () { + var flatpak_manifest = get_flatpak_manifest (); + if (flatpak_manifest != null) { + if (is_running_flatpaked) { + return yield run_command_async ({ + "flatpak-spawn", + "--host", + "flatpak-builder", + "--force-clean", + flatpak_manifest.build_dir, + flatpak_manifest.manifest + }); + } else { + return yield run_command_async ({ + "flatpak-builder", + "--force-clean", + flatpak_manifest.build_dir, + flatpak_manifest.manifest + }); + } + } + + return false; + } + + private async bool run_project () { + var flatpak_manifest = get_flatpak_manifest (); + if (flatpak_manifest != null) { + if (is_running_flatpaked) { + return yield run_command_async ({ + "flatpak-spawn", + "--host", + "flatpak-builder", + "--run", + flatpak_manifest.build_dir, + flatpak_manifest.manifest, + flatpak_manifest.command + }); + } else { + return yield run_command_async ({ + "flatpak-builder", + "--run", + flatpak_manifest.build_dir, + flatpak_manifest.manifest, + flatpak_manifest.command + }); + } + } + + return false; + } + + public async bool build () { + if (is_running) { + debug ("Project “%s“ is already running", project_path); + return false; + } + + was_stopped = false; + on_clear (); + + is_running = true; + var result = yield build_project (); + is_running = false; + + return result; + } + + public async bool build_install_run () { + if (is_running) { + debug ("Project “%s“ is already running", project_path); + return false; + } + + was_stopped = false; + on_clear (); + + is_running = true; + if (!yield build_project ()) { + is_running = false; + return false; + } + + var result = yield run_project (); + is_running = false; + + return result; + } + + public bool stop () { + if (!is_running) { + debug ("Project “%s“ is not running", project_path); + return true; + } + + was_stopped = true; + var result = Posix.kill (command_pid, Posix.Signal.TERM) == 0; + is_running = !result; + + return result; + } +} diff --git a/src/Widgets/ChooseProjectButton.vala b/src/Widgets/ChooseProjectButton.vala index 4b4759ecdf..f2b260d903 100644 --- a/src/Widgets/ChooseProjectButton.vala +++ b/src/Widgets/ChooseProjectButton.vala @@ -99,6 +99,7 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { label_widget.label = _(NO_PROJECT_SELECTED); label_widget.tooltip_text = _("Active Git project: %s").printf (_(NO_PROJECT_SELECTED)); Scratch.Services.GitManager.get_instance ().active_project_path = ""; + Scratch.Services.ProjectManager.get_instance ().project_path = null; } }); @@ -137,6 +138,7 @@ public class Code.ChooseProjectButton : Gtk.MenuButton { label_widget.tooltip_text = _("Active Git project: %s").printf (project_entry.project_path); project_entry.active = true; Scratch.Services.GitManager.get_instance ().active_project_path = project_entry.project_path; + Scratch.Services.ProjectManager.get_instance ().project_path = project_entry.project_path; } public void set_document (Scratch.Services.Document doc) { diff --git a/src/Widgets/HeaderBar.vala b/src/Widgets/HeaderBar.vala index 2ca4efef59..a719ff18c7 100644 --- a/src/Widgets/HeaderBar.vala +++ b/src/Widgets/HeaderBar.vala @@ -28,6 +28,9 @@ namespace Scratch.Widgets { public Gtk.Button templates_button; public Code.FormatBar format_bar; public Code.ChooseProjectButton choose_project_button; + public Gtk.Button build_button; + public Gtk.Button run_button; + public Gtk.Button stop_button; public Gtk.Revealer choose_project_revealer; private const string STYLE_SCHEME_HIGH_CONTRAST = "classic"; @@ -77,6 +80,27 @@ namespace Scratch.Widgets { _("Save this file with a different name") ); + build_button = new Gtk.Button.from_icon_name ("media-playlist-repeat", Gtk.IconSize.LARGE_TOOLBAR); + build_button.action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_BUILD; + build_button.tooltip_markup = Granite.markup_accel_tooltip ( + app_instance.get_accels_for_action (build_button.action_name), + _("Build") + ); + + run_button = new Gtk.Button.from_icon_name ("media-playback-start", Gtk.IconSize.LARGE_TOOLBAR); + run_button.action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_RUN; + run_button.tooltip_markup = Granite.markup_accel_tooltip ( + app_instance.get_accels_for_action (run_button.action_name), + _("Run") + ); + + stop_button = new Gtk.Button.from_icon_name ("media-playback-stop", Gtk.IconSize.LARGE_TOOLBAR); + stop_button.action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_STOP; + stop_button.tooltip_markup = Granite.markup_accel_tooltip ( + app_instance.get_accels_for_action (stop_button.action_name), + _("Stop") + ); + var revert_button = new Gtk.Button.from_icon_name ("document-revert", Gtk.IconSize.LARGE_TOOLBAR); revert_button.action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_REVERT; revert_button.tooltip_markup = Granite.markup_accel_tooltip ( @@ -194,6 +218,10 @@ namespace Scratch.Widgets { pack_start (save_button); pack_start (save_as_button); pack_start (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); + pack_start (build_button); + pack_start (run_button); + pack_start (stop_button); + pack_start (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); pack_start (revert_button); pack_end (app_menu); pack_end (share_app_menu); diff --git a/src/Widgets/Terminal.vala b/src/Widgets/Terminal.vala new file mode 100644 index 0000000000..44b5908c4c --- /dev/null +++ b/src/Widgets/Terminal.vala @@ -0,0 +1,73 @@ +/*- + * Copyright 2019-2021 elementary, Inc. (https://elementary.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Michael Aaron Murphy + */ + +public class Scratch.Widgets.Terminal : Gtk.ScrolledWindow { + public signal void toggled (bool active); + public Gtk.TextBuffer buffer { get; construct; } + + private Gtk.TextView view; + private double prev_upper_adj = 0; + + public string log { + owned get { + return view.buffer.text; + } + } + + public Terminal (Gtk.TextBuffer buffer) { + Object (buffer: buffer); + } + + construct { + view = new Gtk.TextView.with_buffer (buffer) { + cursor_visible = true, + editable = false, + margin_end = 6, + margin_start = 6, + monospace = true, + pixels_below_lines = 3, + wrap_mode = Gtk.WrapMode.WORD + }; + view.get_style_context ().remove_class (Gtk.STYLE_CLASS_VIEW); + + hscrollbar_policy = Gtk.PolicyType.NEVER; + expand = true; + min_content_height = 120; + add (view); + get_style_context ().add_class (Granite.STYLE_CLASS_TERMINAL); + + view.size_allocate.connect (() => attempt_scroll ()); + } + + public void attempt_scroll () { + var adj = vadjustment; + + var units_from_end = prev_upper_adj - adj.page_size - adj.value; + var view_size_difference = adj.upper - prev_upper_adj; + if (view_size_difference < 0) { + view_size_difference = 0; + } + + if (prev_upper_adj <= adj.page_size || units_from_end <= 50) { + adj.value = adj.upper; + } + + prev_upper_adj = adj.upper; + } +} diff --git a/src/meson.build b/src/meson.build index 8404a86ebb..af7ac0dc9d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -33,6 +33,7 @@ code_files = files( 'Services/GitManager.vala', 'Services/MonitoredRepository.vala', 'Services/PluginManager.vala', + 'Services/ProjectManager.vala', 'Services/Settings.vala', 'Services/TemplateManager.vala', 'Widgets/ChooseProjectButton.vala', @@ -44,6 +45,7 @@ code_files = files( 'Widgets/PaneSwitcher.vala', 'Widgets/SearchBar.vala', 'Widgets/SourceView.vala', + 'Widgets/Terminal.vala', 'Widgets/WelcomeView.vala', )