25 |
26 | ## Features
27 |
28 | - Color schemes - ([Tilix](https://github.com/gnunn1/tilix) compatible color scheme support)
29 | - Theming - your color scheme can be used to style the whole app
30 | - Background transparency
31 | - Custom fonts, padding, and cell spacing
32 | - Tabs
33 | - Support for drag and dropping files
34 | - Sixel (experimental)
35 | - Customizable keybindings
36 | - Toggle-able header bar
37 | - Search your backlog with text or regex
38 | - Context aware header bar - the header bar changes colors when running commands with sudo and in ssh sessions
39 | - Desktop notifications - get notified when a command is finished in the background
40 | - Customizable UI
41 |
42 | ## Install
43 |
44 | **Flathub**
45 |
46 |
47 |
48 | ```bash
49 | flatpak install flathub com.raggesilver.BlackBox
50 | ```
51 |
52 | **Flatpak Nightly**
53 |
54 | You can also download the most recent build. Note that these are _unstable_ and completely unavailable if the latest pipeline failed.
55 |
56 | - [Flatpak](https://gitlab.gnome.org/raggesilver/blackbox/-/jobs/artifacts/main/raw/blackbox.flatpak?job=flatpak)
57 | - [Zip](https://gitlab.gnome.org/raggesilver/blackbox/-/jobs/artifacts/main/download?job=flatpak)
58 |
59 | **Looking for an older release?**
60 |
61 | Check out the [releases page](https://gitlab.gnome.org/raggesilver/blackbox/-/releases).
62 |
63 | ## Compile
64 |
65 | **Flatpak**
66 |
67 | To build and run Black Box, use GNOME Builder or VS Code along with [Vala](https://marketplace.visualstudio.com/items?itemName=prince781.vala) and [Flatpak](https://marketplace.visualstudio.com/items?itemName=bilelmoussaoui.flatpak-vscode) extensions.
68 |
69 | If you want to build Black Box manually, look at the build script in [.gitlab-ci.yml](./.gitlab-ci.yml).
70 |
71 | ## Translations
72 |
73 | Black Box is accepting translations through Weblate! If you'd like to
74 | contribute with translations, visit the
75 | [Weblate project](https://hosted.weblate.org/projects/blackbox/).
76 |
77 |
78 |
79 |
80 |
81 | ## Gallery
82 |
83 | > Some of these screenshot are from older versions of Black Box.
84 |
85 |
86 |
87 |
88 | Black Box with "show header bar" off.
89 |
90 |
91 |
92 |
93 | Black Box with transparent background* and sixel support. *blur is controled
94 | by your compositor.
95 |
96 |
Added shortcut for moving tabs Shift+Ctrl+PageDown/PageUp.
38 |
Ctrl+PageDown/PageUp have been added as default keybindins for switching tabs, alongside (Shift)+Ctrl+Tab (yes, there are two default keybindings). You may need to reset keybindings for these two actions to see the new defaults.
39 |
The window title is now set to the title of the active tab. This is noticeable when hovering Black Box in the GNOME Overview
40 |
Black Box will show a visual indicator on a tab when a command finishes in the background (similar to desktop notifications, but less noisy)
41 |
42 |
Improvements
43 |
44 |
Command completion notifications have been improved. Clicking a notification will now focus the correct tab. Black Box will no longer emit two notifications for the same command.
45 |
46 | """
47 | };
48 |
49 | if (DEVEL) {
50 | window.add_css_class ("devel");
51 | }
52 |
53 | window.add_link (_("Donate"), "https://www.patreon.com/raggesilver");
54 | window.add_link (_("Full Changelog"), "https://gitlab.gnome.org/raggesilver/blackbox/-/blob/main/CHANGELOG.md");
55 |
56 | return window;
57 | }
58 |
59 | private string get_debug_information () {
60 | var app = "Black Box: %s\n".printf (VERSION);
61 | var backend = "Backend: %s\n".printf (get_gtk_backend ());
62 | var renderer = "Renderer: %s\n".printf (get_renderer ());
63 | var flatpak = get_flatpak_info ();
64 | var os_info = get_os_info ();
65 | var libs = get_libraries_info ();
66 |
67 | return app + backend + renderer + flatpak + os_info + libs;
68 | }
69 |
70 | private string get_gtk_backend () {
71 | var display = Gdk.Display.get_default ();
72 | switch (display.get_class ().get_name ()) {
73 | case "GdkX11Display": return "X11";
74 | case "GdkWaylandDisplay": return "Wayland";
75 | case "GdkBroadwayDisplay": return "Broadway";
76 | case "GdkWin32Display": return "Windows";
77 | case "GdkMacosDisplay": return "macOS";
78 | default: return display.get_class ().get_name ();
79 | }
80 | }
81 |
82 | private string get_renderer () {
83 | var display = Gdk.Display.get_default ();
84 | var surface = new Gdk.Surface.toplevel (display);
85 | var renderer = Gsk.Renderer.for_surface (surface);
86 |
87 | var name = renderer.get_class ().get_name ();
88 | renderer.unrealize ();
89 |
90 | switch (name) {
91 | case "GskVulkanRenderer": return "Vulkan";
92 | case "GskGLRenderer": return "GL";
93 | case "GskCairoRenderer": return "Cairo";
94 | default: return name;
95 | }
96 | }
97 |
98 | static KeyFile? flatpak_keyfile = null;
99 |
100 | private string? get_flatpak_value (string group, string key) {
101 | try {
102 | if (flatpak_keyfile == null) {
103 | flatpak_keyfile = new KeyFile ();
104 | flatpak_keyfile.load_from_file ("/.flatpak-info", 0);
105 | }
106 | return flatpak_keyfile.get_string (group, key);
107 | }
108 | catch (Error e) {
109 | warning ("%s", e.message);
110 | return null;
111 | }
112 | }
113 |
114 | private string get_flatpak_info () {
115 | #if BLACKBOX_IS_FLATPAK
116 | string res = "Flatpak:\n";
117 |
118 | res += " - Runtime: %s\n".printf (get_flatpak_value ("Application", "runtime"));
119 | res += " - Runtime commit: %s\n".printf (get_flatpak_value ("Instance", "runtime-commit"));
120 | res += " - Arch: %s\n".printf (get_flatpak_value ("Instance", "arch"));
121 | res += " - Flatpak version: %s\n".printf (get_flatpak_value ("Instance", "flatpak-version"));
122 | res += " - Devel: %s\n".printf (get_flatpak_value ("Instance", "devel") != null ? "yes" : "no");
123 |
124 | return res;
125 | #else
126 | return "Flatpak: No\n";
127 | #endif
128 | }
129 |
130 | private string get_libraries_info () {
131 | string res = "Libraries:\n";
132 |
133 | res += " - Gtk: %d.%d.%d\n".printf (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION);
134 | res += " - VTE: %d.%d.%d\n".printf (Vte.MAJOR_VERSION, Vte.MINOR_VERSION, Vte.MICRO_VERSION);
135 | res += " - Libadwaita: %s\n".printf (Adw.VERSION_S);
136 | res += " - JSON-glib: %s\n".printf (Json.VERSION_S);
137 |
138 | return res;
139 | }
140 |
141 | private string get_os_info () {
142 | string res = "OS:\n";
143 |
144 | res += " - Name: %s\n".printf (Environment.get_os_info (OsInfoKey.NAME));
145 | res += " - Version: %s\n".printf (Environment.get_os_info (OsInfoKey.VERSION));
146 |
147 | return res;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/widgets/TerminalTab.vala:
--------------------------------------------------------------------------------
1 | /* TerminalTab.vala
2 | *
3 | * Copyright 2021-2022 Paulo Queiroz
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | [GtkTemplate (ui = "/com/raggesilver/BlackBox/gtk/terminal-tab.ui")]
20 | public class Terminal.TerminalTab : Gtk.Box {
21 |
22 | // This signal is emitted when the TerminalTab is asking to be closed.
23 | public signal void close_request ();
24 |
25 | [GtkChild] unowned Adw.Banner banner;
26 | [GtkChild] unowned Gtk.ScrolledWindow scrolled;
27 | [GtkChild] unowned SearchToolbar search_toolbar;
28 |
29 | private string default_title;
30 | private Gtk.PopoverMenu popover;
31 |
32 | public Terminal terminal { get; protected set; }
33 | public string? title_override { get; private set; default = null; }
34 |
35 | public string title {
36 | get {
37 | if (this.title_override != null) return this.title_override;
38 | if (this.terminal.window_title != "") return this.terminal.window_title;
39 |
40 | return this.default_title;
41 | }
42 | }
43 |
44 | static construct {
45 | typeof (SearchToolbar).class_ref ();
46 | }
47 |
48 | public TerminalTab (Window window,
49 | uint tab_id,
50 | string? command,
51 | string? cwd)
52 | {
53 | Object (
54 | orientation: Gtk.Orientation.VERTICAL,
55 | spacing: 0
56 | );
57 |
58 | this.default_title = command ?? "%s %u".printf (_("tab"), tab_id);
59 |
60 | this.terminal = new Terminal (window, command, cwd);
61 | // TODO: Can't we use a property for this? Has default or something?
62 | this.terminal.grab_focus ();
63 | this.popover = build_popover();
64 |
65 | var click = new Gtk.GestureClick () {
66 | button = Gdk.BUTTON_SECONDARY,
67 | };
68 |
69 | click.pressed.connect (this.show_menu);
70 |
71 | this.terminal.add_controller (click);
72 |
73 | this.connect_signals ();
74 | }
75 |
76 | #if BLACKBOX_DEBUG_MEMORY
77 | ~TerminalTab () {
78 | message ("TerminalTab destroyed");
79 | }
80 |
81 | public override void dispose () {
82 | message ("TerminalTab dispose");
83 | base.dispose ();
84 | }
85 | #endif
86 |
87 | private void connect_signals () {
88 | var settings = Settings.get_default ();
89 |
90 | // this.terminal.bind_property ("window-title",
91 | // this,
92 | // "title",
93 | // GLib.BindingFlags.DEFAULT,
94 | // null, null);
95 |
96 | this.terminal.notify ["window-title"].connect (() => {
97 | this.notify_property ("title");
98 | });
99 |
100 | this.notify ["title-override"].connect (() => {
101 | this.notify_property ("title");
102 | });
103 |
104 | this.terminal.exit.connect (() => {
105 | this.close_request ();
106 | });
107 |
108 | this.terminal.spawn_failed.connect ((message) => {
109 | this.override_title (_("Error"));
110 | this.banner.title = message;
111 | this.banner.revealed = true;
112 | });
113 |
114 | settings.notify ["show-scrollbars"]
115 | .connect (this.on_show_scrollbars_updated);
116 |
117 | settings.notify_property ("show-scrollbars");
118 |
119 | settings.schema.bind (
120 | "use-overlay-scrolling",
121 | this.scrolled,
122 | "overlay-scrolling",
123 | SettingsBindFlags.GET
124 | );
125 |
126 | settings.bind_property (
127 | "use-sixel",
128 | this.terminal,
129 | "enable-sixel",
130 | BindingFlags.SYNC_CREATE
131 | );
132 |
133 | // refocus terminal after closing context menu, otherwise the focus will go on the header buttons
134 | this.popover.closed.connect_after (pop_close);
135 | }
136 |
137 | private void on_show_scrollbars_updated () {
138 | var settings = Settings.get_default ();
139 | var show_scrollbars = settings.show_scrollbars;
140 | var is_scrollbar_being_used = this.terminal.parent == this.scrolled;
141 |
142 | this.scrolled.visible = show_scrollbars;
143 |
144 | if (show_scrollbars != is_scrollbar_being_used) {
145 | if (this == this.terminal.parent) {
146 | this.remove (this.terminal);
147 | }
148 | else if (this.scrolled == this.terminal.parent) {
149 | this.scrolled.child = null;
150 | }
151 | }
152 |
153 | if (
154 | show_scrollbars != is_scrollbar_being_used ||
155 | this.terminal.parent == null
156 | ) {
157 | if (show_scrollbars) {
158 | this.scrolled.child = this.terminal;
159 | }
160 | else {
161 | this.insert_child_after (this.terminal, null);
162 | }
163 | }
164 | }
165 |
166 | public Gtk.PopoverMenu build_popover () {
167 | var builder = new Gtk.Builder.from_resource ("/com/raggesilver/BlackBox/gtk/terminal-menu.ui");
168 | var pop = builder.get_object ("popover") as Gtk.PopoverMenu;
169 |
170 | pop.set_parent (this);
171 | pop.set_has_arrow (false);
172 | pop.set_halign (Gtk.Align.START);
173 |
174 | return pop;
175 | }
176 |
177 | public void pop_close() {
178 | this.terminal.grab_focus();
179 | }
180 |
181 | public void show_menu (int n_pressed, double x, double y) {
182 | if (this.terminal.hyperlink_hover_uri != null) {
183 | this.terminal.window.link = this.terminal.hyperlink_hover_uri;
184 | } else {
185 | this.terminal.window.link = this.terminal.check_match_at (x, y, null);
186 | }
187 |
188 | double x_in_view, y_in_view;
189 | this.terminal.translate_coordinates (this, x, y, out x_in_view, out y_in_view);
190 |
191 | var r = Gdk.Rectangle () {
192 | x = (int) x_in_view,
193 | y = (int) y_in_view
194 | };
195 |
196 | this.popover.set_pointing_to (r);
197 | this.popover.popup ();
198 | }
199 |
200 | public void search () {
201 | this.search_toolbar.open ();
202 | }
203 |
204 | public void override_title (string? _title) {
205 | this.title_override = _title;
206 | }
207 |
208 | public uint get_id () {
209 | return terminal.id;
210 | }
211 |
212 | public void on_before_close () {
213 | terminal.on_before_close ();
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/src/resources/svg/color-scheme-thumbnail.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
199 |
--------------------------------------------------------------------------------
/src/widgets/SearchToolbar.vala:
--------------------------------------------------------------------------------
1 | /* SearchToolbar.vala
2 | *
3 | * Copyright 2022 Paulo Queiroz
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | *
18 | * SPDX-License-Identifier: GPL-3.0-or-later
19 | */
20 |
21 | [GtkTemplate (ui = "/com/raggesilver/BlackBox/gtk/search-toolbar.ui")]
22 | public class Terminal.SearchToolbar : Gtk.Widget {
23 |
24 | [GtkChild] private unowned Gtk.Button next_button;
25 | [GtkChild] private unowned Gtk.Button previous_button;
26 | [GtkChild] private unowned Gtk.CheckButton match_case_sensitive_check_button;
27 | [GtkChild] private unowned Gtk.CheckButton match_regex_check_button;
28 | [GtkChild] private unowned Gtk.CheckButton match_whole_words_check_button;
29 | [GtkChild] private unowned Gtk.CheckButton wrap_around_check_button;
30 | [GtkChild] private unowned Gtk.Revealer revealer;
31 | [GtkChild] private unowned Gtk.SearchEntry search_entry;
32 |
33 | public weak Terminal terminal { get; set; }
34 |
35 | static construct {
36 | set_layout_manager_type (typeof (Gtk.BinLayout));
37 | }
38 |
39 | construct {
40 | this.search_entry.set_key_capture_widget (this);
41 |
42 | if (this.terminal != null) {
43 | this.bind_data ();
44 | this.connect_signals ();
45 | }
46 | else {
47 | ulong handler;
48 |
49 | handler = this.notify ["terminal"].connect (() => {
50 | if (this.terminal == null) return;
51 |
52 | this.disconnect (handler);
53 |
54 | this.bind_data ();
55 | this.connect_signals ();
56 | });
57 | }
58 | }
59 |
60 | public override void dispose () {
61 | this.revealer.unparent ();
62 | base.dispose ();
63 | }
64 |
65 | public void open () {
66 | this.revealer.reveal_child = true;
67 | this.search_entry.grab_focus ();
68 |
69 | if (this.terminal.get_has_selection ()) {
70 | var text = this.terminal.get_text_selected (
71 | Vte.Format.TEXT
72 | );
73 | this.terminal.unselect_all ();
74 | this.search_entry.text = text;
75 | }
76 | }
77 |
78 | public void close () {
79 | this.revealer.reveal_child = false;
80 | this.search_entry.text = "";
81 | this.terminal.unselect_all ();
82 | this.terminal.match_remove_all ();
83 | this.terminal.search_set_regex (null, 0);
84 | }
85 |
86 | private bool on_key_pressed (uint keyval, uint _kc, Gdk.ModifierType _mod) {
87 | if (keyval == Gdk.Key.Escape) {
88 | this.close ();
89 | return Gdk.EVENT_STOP;
90 | }
91 | return Gdk.EVENT_PROPAGATE;
92 | }
93 |
94 | private void bind_data () {
95 | var ssetings = SearchSettings.get_default ();
96 |
97 | ssetings.schema.bind (
98 | "wrap-around",
99 | this.wrap_around_check_button,
100 | "active",
101 | SettingsBindFlags.DEFAULT
102 | );
103 |
104 | ssetings.schema.bind (
105 | "match-whole-words",
106 | this.match_whole_words_check_button,
107 | "active",
108 | SettingsBindFlags.DEFAULT
109 | );
110 |
111 | ssetings.schema.bind (
112 | "match-case-sensitive",
113 | this.match_case_sensitive_check_button,
114 | "active",
115 | SettingsBindFlags.DEFAULT
116 | );
117 |
118 | ssetings.schema.bind (
119 | "match-regex",
120 | this.match_regex_check_button,
121 | "active",
122 | SettingsBindFlags.DEFAULT
123 | );
124 | }
125 |
126 | private void connect_signals () {
127 | var ssettings = SearchSettings.get_default ();
128 |
129 | this.search_entry.search_changed.connect (this.do_search);
130 | this.search_entry.activate.connect (this.search_previous);
131 | this.search_entry.search_started.connect (() => {
132 | if (!this.search_entry.has_focus) {
133 | this.search_entry.grab_focus ();
134 | }
135 | });
136 |
137 | this.previous_button.clicked.connect (this.search_previous);
138 | this.next_button.clicked.connect (this.search_next);
139 |
140 | this.terminal.search_set_wrap_around (this.wrap_around_check_button.active);
141 | this.wrap_around_check_button.notify ["active"].connect (() => {
142 | this.terminal.search_set_wrap_around (
143 | this.wrap_around_check_button.active
144 | );
145 | });
146 |
147 | var kpc = new Gtk.EventControllerKey ();
148 | kpc.key_pressed.connect (this.on_key_pressed);
149 | this.search_entry.add_controller (kpc);
150 |
151 | kpc = new Gtk.EventControllerKey ();
152 | kpc.key_pressed.connect (this.on_key_pressed);
153 | (this as Gtk.Widget)?.add_controller (kpc);
154 |
155 | ssettings.notify.connect ((spec) => {
156 | // If any search match related properties changed call search again
157 | if (spec.name.has_prefix ("match-")) {
158 | this.do_search ();
159 | }
160 | });
161 | }
162 |
163 | public void search (string text) {
164 | this.search_entry.set_text (text);
165 | }
166 |
167 | private void do_search () {
168 | var text = this.search_entry.text;
169 |
170 | if (text == null || text == "") {
171 | this.terminal.unselect_all ();
172 | this.terminal.match_remove_all ();
173 | this.terminal.search_set_regex (null, 0);
174 | return;
175 | }
176 |
177 | this.terminal.search_find_next ();
178 |
179 | string search_string = null;
180 | var ssettings = SearchSettings.get_default ();
181 | PCRE2.Flags search_flags = PCRE2.Flags.MULTILINE;
182 |
183 | if (ssettings.match_regex) {
184 | search_string = text;
185 | }
186 | else {
187 | search_string = Regex.escape_string (text);
188 | }
189 |
190 | if (ssettings.match_whole_words) {
191 | search_string = "\\b%s\\b".printf (search_string);
192 | }
193 |
194 | if (!ssettings.match_case_sensitive) {
195 | search_flags |= PCRE2.Flags.CASELESS;
196 | }
197 |
198 | try {
199 | this.terminal.search_set_regex (
200 | new Vte.Regex.for_search (search_string, -1, search_flags),
201 | 0
202 | );
203 |
204 | // Auto-select the last (most recent) result
205 | this.terminal.search_find_previous ();
206 | }
207 | catch (Error e) {
208 | warning ("%s", e.message);
209 | }
210 | }
211 |
212 | private void search_next () {
213 | this.terminal.search_find_next ();
214 | }
215 |
216 | private void search_previous () {
217 | this.terminal.search_find_previous ();
218 | }
219 |
220 | [GtkCallback]
221 | private void on_close_button_pressed () {
222 | this.close ();
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/src/widgets/ColorSchemeThumbnail.vala:
--------------------------------------------------------------------------------
1 | /* ColorSchemeThumbnail.vala
2 | *
3 | * Copyright 2021-2022 Paulo Queiroz
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | /**
20 | * Used to load contents of "color-scheme-thumbnail.svg" as a color scheme
21 | * thumbnail template. It can convert {@link Terminal.Scheme} to string that
22 | * contains an edited version of "color-scheme-thumbnail.svg".
23 | */
24 | public class Terminal.ColorSchemeThumbnailProvider {
25 | private static string svg_content = null;
26 |
27 | public static void init_resource () {
28 | if (svg_content == null) {
29 | try {
30 | uint8[] data;
31 |
32 | GLib.File.new_for_uri (
33 | "resource:///com/raggesilver/BlackBox/resources/svg/color-scheme-thumbnail.svg"
34 | ).load_contents (null, out data, null);
35 |
36 | svg_content = (string) data;
37 | }
38 | catch (Error e) {
39 | error ("%s", e.message);
40 | }
41 | }
42 | }
43 |
44 | private static void process_node (Xml.Node *node, Scheme scheme) {
45 | if (node == null) {
46 | return ;
47 | }
48 |
49 | Gdk.RGBA? color = null;
50 |
51 | if (node->get_prop ("label") == "palette") {
52 | int len = (int) scheme.palette.length;
53 | color = scheme.palette.index (Random.int_range (7, len));
54 | }
55 |
56 | if (node->get_prop ("label") == "fg") {
57 | color = scheme.foreground_color;
58 | }
59 |
60 | if (color != null) {
61 | node->set_prop (
62 | "style",
63 | "stroke:%s;stroke-width:3;stroke-linecap:round;".printf (
64 | color.to_string ()
65 | )
66 | );
67 | }
68 |
69 | for (
70 | var child = node->children;
71 | child != null;
72 | child = child->next_element_sibling ()
73 | ) {
74 | process_node (child, scheme);
75 | }
76 | }
77 |
78 | public static uint8[]? apply_scheme (Scheme scheme) {
79 | // Parse svg_content into XML document
80 | var doc = Xml.Parser.parse_memory (svg_content, svg_content.data.length);
81 |
82 | // Edit XML document according to color scheme
83 | process_node (doc->get_root_element (), scheme);
84 |
85 | // Convert edited XML document to string
86 | string str;
87 | doc->dump_memory (out str, null);
88 |
89 | var data = str.data.copy ();
90 |
91 | delete doc;
92 | return data;
93 | }
94 | }
95 |
96 | public class Terminal.ColorSchemePreviewPaintable : GLib.Object, Gdk.Paintable {
97 | private Rsvg.Handle? handler;
98 | private Scheme scheme;
99 |
100 | public ColorSchemePreviewPaintable (Scheme scheme) {
101 | this.scheme = scheme;
102 | this.load_image.begin ();
103 | }
104 |
105 | public void snapshot (Gdk.Snapshot snapshot, double width, double height) {
106 | var cr = (snapshot as Gtk.Snapshot)?.append_cairo (
107 | Graphene.Rect ().init (0, 0, (float) width, (float) height)
108 | );
109 | try {
110 | this.handler.render_document (cr, Rsvg.Rectangle () {
111 | x = 0,
112 | y = 0,
113 | width = width,
114 | height = height
115 | });
116 | }
117 | catch (Error e) {
118 | // TODO: should we make this a warning? It seems a bit overkill to crash
119 | // the app because we can't render a thumbnail
120 | error ("%s", e.message);
121 | }
122 | }
123 |
124 | // Methods
125 |
126 | private async void load_image () {
127 | var file_content = ColorSchemeThumbnailProvider.apply_scheme (this.scheme);
128 | if (file_content == null) return;
129 |
130 | try {
131 | this.handler = new Rsvg.Handle.from_data (file_content);
132 | }
133 | catch (Error e) {
134 | error ("%s", e.message);
135 | }
136 | }
137 | }
138 |
139 | /**
140 | * Thumbnail of color scheme
141 | * Base on GtkSourceStyleSchemePreview:
142 | * https://gitlab.gnome.org/GNOME/gtksourceview/-/blob/master/gtksourceview/gtksourcestyleschemepreview.c
143 | */
144 | public class Terminal.ColorSchemeThumbnail : Gtk.FlowBoxChild {
145 | public bool selected { get; set; }
146 | public Scheme scheme { get; set; }
147 |
148 | public ColorSchemeThumbnail (Scheme scheme) {
149 | Object (has_tooltip: true, scheme: scheme);
150 |
151 | this.tooltip_text = scheme.name;
152 | this.add_css_class ("thumbnail");
153 |
154 | // The color scheme thumbnail
155 | var img = new Gtk.Picture () {
156 | paintable = new ColorSchemePreviewPaintable (scheme),
157 | width_request = 110,
158 | height_request = 70,
159 | // height_request = 90,
160 | css_classes = { "card" },
161 | cursor = new Gdk.Cursor.from_name ("pointer", null),
162 | };
163 |
164 | var css_provider = get_css_provider_for_data (
165 | // "picture { background-color: %s; padding-bottom: 2em; }".printf (
166 | "picture { background-color: %s; }".printf (
167 | scheme.background_color.to_string ()
168 | )
169 | );
170 |
171 | img.get_style_context ().add_provider (
172 | css_provider,
173 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
174 | );
175 |
176 | // Icon will show when this.selected is true
177 | var checkicon = new Gtk.Image () {
178 | icon_name = "object-select-symbolic",
179 | pixel_size = 14,
180 | vexpand = true,
181 | valign = Gtk.Align.END,
182 | halign = Gtk.Align.END,
183 | visible = false,
184 | };
185 |
186 | img.set_parent (this);
187 | checkicon.set_parent (this);
188 |
189 | // var lbl = new Gtk.Label (scheme.name) {
190 | // ellipsize = Pango.EllipsizeMode.END,
191 | // halign = Gtk.Align.CENTER,
192 | // hexpand = true,
193 | // justify = Gtk.Justification.CENTER,
194 | // valign = Gtk.Align.END,
195 | // wrap = false,
196 | // xalign = 0.5f,
197 | // };
198 |
199 | // PQMarble.set_theming_for_data (lbl, "label { color: %s; margin: 0.5em 8px; }".printf(scheme.foreground_color.to_string ()), null, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
200 |
201 | // lbl.set_parent (this);
202 |
203 | this.notify["selected"].connect (() => {
204 | if (this.selected) {
205 | img.add_css_class ("selected");
206 | }
207 | else {
208 | img.remove_css_class ("selected");
209 | }
210 | checkicon.visible = this.selected;
211 | });
212 |
213 | // Emit activate signal when thumbnail is chicked.
214 | var mouse_control = new Gtk.GestureClick ();
215 | mouse_control.pressed.connect (() => {
216 | if (!this.selected) {
217 | this.selected = true;
218 | this.activate ();
219 | }
220 | });
221 | img.add_controller (mouse_control);
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/data/com.raggesilver.BlackBox.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 600
6 | Window width
7 |
8 |
9 | 350
10 | Window height
11 |
12 |
13 | false
14 | Whether or not to remember window size
15 |
16 |
17 | false
18 |
19 |
20 | false
21 |
22 |
23 | 'Monospace 12'
24 | Font family and size
25 |
26 |
27 | true
28 | Whether the window should inherit terminal theme's colors
29 |
30 |
31 | 100
32 | Terminal window background opacity
33 |
34 |
35 | true
36 | Whether or not tabs should expand to fill tab bar
37 |
38 |
39 | true
40 | Whether the headerbar should be shown or not
41 |
42 |
43 | true
44 | If enabled, the header bar will be colored differently for root and ssh contexts
45 |
46 |
47 | true
48 | Whether or not to display a menu button in the headerbar
49 |
50 |
51 | false
52 | Whether or not to reserve an area for dragging the header bar
53 |
54 |
55 | true
56 | Whether or not to show scrollbars
57 |
58 |
59 | true
60 | Whether overlay scrolling should be enabled
61 |
62 |
63 | false
64 | If enabled, terminals will scroll by pixels instead of rows
65 |
66 |
67 | false
68 | If enabled, terminals will render sixel escape sequences
69 |
70 |
71 | 0
72 |
73 | Scrollback mode
74 |
75 |
76 | 1000
77 | Number of lines stored in scrollback
78 |
79 |
80 | false
81 | If enabled, floating controls will be shown in headerless mode
82 |
83 |
84 | 10
85 | Hoverable area (in pixels) at the top of the window to trigger floating controls
86 |
87 |
88 | 400
89 | Delay time before showing floating controls
90 |
91 |
92 | 'Adwaita'
93 | The light color scheme for the terminal
94 |
95 |
96 | 'Adwaita Dark'
97 | The dark color scheme for the terminal
98 |
99 |
100 | 0
101 |
102 | Style preference
103 |
104 |
105 | false
106 | Show bold text in bright colors
107 |
108 |
109 | false
110 | Hide cursor while typing
111 |
112 |
113 | (0,0,0,0)
114 | Amount of padding around terminal widgets (top, right, bottom, left)
115 |
116 |
117 | 1.0
118 | Terminal cell width
119 |
120 |
121 | 1.0
122 | Terminal cell height
123 |
124 |
125 | true
126 | Terminal bell
127 |
128 |
129 | 0
130 | Cursor shape
131 |
132 |
133 | 0
134 | Whether or not the cursor should blink
135 |
136 |
137 | false
138 | If enabled, ctrl+c and ctrl+v will work for copy/paste
139 |
140 |
141 | true
142 | Whether to spawn shell in login mode
143 |
144 |
145 | false
146 | Whether to use a custom command instead of the user's shell
147 |
148 |
149 | ''
150 | Custom command to use instead of the user's shell
151 |
152 |
153 | true
154 | Send a desktop notification when a process is completed on an unfocussed tab
155 |
156 |
157 | 0
158 |
159 | Working directory mode
160 |
161 |
162 | '~'
163 | Custom working directory for new terminals
164 |
165 |
166 |
167 |
169 |
170 | true
171 | Whether clicking next on the last search result should return to the first
172 |
173 |
174 | false
175 | Whether search should be case sensitive
176 |
177 |
178 | false
179 | Whether search should only match entire words
180 |
181 |
182 | false
183 | Whether search should be performed using regular expressions
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------
/src/services/ProcessWatcher.vala:
--------------------------------------------------------------------------------
1 | /* ProcessWatcher.vala
2 | *
3 | * Copyright 2023 Paulo Queiroz
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | *
18 | * SPDX-License-Identifier: GPL-3.0-or-later
19 | */
20 |
21 | public enum Terminal.ProcessContext {
22 | DEFAULT,
23 | ROOT,
24 | SSH
25 | }
26 |
27 | public class Terminal.Process : Object {
28 | /**
29 | * This signal is emitted when the foreground task of a shell finishes.
30 | */
31 | public signal void foreground_task_finished ();
32 |
33 | /**
34 | * This is the file descriptor used by the terminal we're tracking. This must
35 | * be set during instanciation of this class and may not be modified later.
36 | */
37 | public int terminal_fd { get; construct set; }
38 |
39 | /**
40 | * This is the controlling PID for a terminal session. It will point to the
41 | * user's shell, in most cases. If the terminal was created with a different
42 | * command (i.e., `blackbox --command "sleep 300"`), this will point to the
43 | * spawned process instead.
44 | */
45 | public Pid pid { get; set; default = -1; }
46 |
47 | /**
48 | * This is the PID of the process currently running at the top of the user's
49 | * shell (e.g., if the user opened a terminal with bash, then opened Neovim
50 | * with `nvim`, the foreground task for this session, and, consequently, this
51 | * PID, will point to Neovim).
52 | */
53 | public Pid foreground_pid { get; set; default = -1; }
54 |
55 | public string? last_foreground_task_command { get; set; default = null; }
56 |
57 | // TODO: we might want to keep track of background PIDs as well (if that's
58 | // even possible). That will allow us to alert the user of potential
59 | // background tasks that would be lost upon closing the tab.
60 |
61 | /***/
62 | public bool ended { get; set; default = false; }
63 |
64 | public ProcessContext context { get; set; default = ProcessContext.DEFAULT; }
65 | }
66 |
67 | namespace Terminal {
68 | const uint PROCESS_WATCHER_INTERVAL_MS = 500;
69 | }
70 |
71 | public class Terminal.ProcessWatcher : Object {
72 |
73 | private static ProcessWatcher? instance = null;
74 | private Gee.ArrayList process_list;
75 | private Gee.ArrayList pending_process_list;
76 | private bool watching = false;
77 | private Thread? loop_thread = null;
78 | private Mutex mutex = Mutex ();
79 |
80 | private ProcessWatcher () {
81 | this.process_list = new Gee.ArrayList ();
82 | this.pending_process_list = new Gee.ArrayList ();
83 | }
84 |
85 | public static ProcessWatcher get_instance () {
86 | if (instance == null) {
87 | instance = new ProcessWatcher ();
88 | }
89 | return instance;
90 | }
91 |
92 | public bool watch (Process process) {
93 | lock (this.pending_process_list) {
94 | this.pending_process_list.add (process);
95 | }
96 |
97 | if (!this.watching) {
98 | this.start_watching ();
99 | }
100 |
101 | return true;
102 | }
103 |
104 | private void start_watching () {
105 | this.watching = true;
106 | this.loop_thread = new Thread ("process-watcher", this.t_watch_loop);
107 | // Timeout.add (PROCESS_WATCHER_INTERVAL_MS, this.watch_loop);
108 | }
109 |
110 | // private void stop_watching () {
111 | // this.mutex.@lock ();
112 | // this.loop_thread.join ();
113 | // this.watching = false;
114 | // this.loop_thread = null;
115 | // this.mutex.@unlock ();
116 | // }
117 |
118 | // This is the watch function that runs in an infinite loop and checks for
119 | // process updates periodically. This function should be executed in a
120 | // separate thread.
121 | private void t_watch_loop () {
122 | // TODO: exit loop when there are no more processes on the list. This will
123 | // happen if:
124 | //
125 | // - We support having Black Box's main window open without any tabs
126 | // - The last tab in a window is closed but a preferences window remais
127 | // open (in which case the app doesn't close)
128 | while (true) {
129 | this.mutex.@lock ();
130 |
131 | lock (this.pending_process_list) {
132 | if (this.pending_process_list.size > 0) {
133 | foreach (var process in this.pending_process_list) {
134 | if (!this.process_list.contains (process)) {
135 | this.process_list.add (process);
136 | }
137 | }
138 | this.pending_process_list.clear ();
139 | }
140 | }
141 |
142 | debug ("Watching %d processes", this.process_list.size);
143 |
144 | if (this.requires_process_watching ()) {
145 | foreach (var process in this.process_list) {
146 | this.check_process (process);
147 | }
148 | }
149 | else {
150 | foreach (var process in this.process_list) {
151 | process.context = ProcessContext.DEFAULT;
152 | }
153 | }
154 |
155 | for (int i = 0; i < this.process_list.size;) {
156 | if (this.process_list.get (i).ended) {
157 | this.process_list.remove_at (i);
158 | }
159 | else {
160 | i++;
161 | }
162 | }
163 |
164 | bool ret = this.process_list.size > 0;
165 | this.watching = ret;
166 | this.mutex.@unlock ();
167 |
168 | if (!ret) {
169 | break;
170 | }
171 |
172 | Thread.usleep (1000 * PROCESS_WATCHER_INTERVAL_MS);
173 | }
174 | }
175 |
176 | private bool requires_process_watching () {
177 | return Settings.get_default ().context_aware_header_bar
178 | && Settings.get_default ().show_headerbar;
179 | }
180 |
181 | private bool is_process_still_running (Pid pid) {
182 | try {
183 | return check_pid_running(pid);
184 | }
185 | catch (Error e) {
186 | warning ("%s", e.message);
187 | }
188 | return false;
189 | }
190 |
191 | private void check_process (Process process) {
192 | try {
193 | // TODO: check if we previously had a foreground process running. If so,
194 | // check that it still is and fire and event if not.
195 |
196 | {
197 | if (
198 | process.foreground_pid >= 0 &&
199 | !is_process_still_running (process.foreground_pid)
200 | ) {
201 | process.foreground_task_finished ();
202 | process.foreground_pid = -1;
203 | }
204 | }
205 |
206 | {
207 | get_foreground_process.begin (process.terminal_fd, null, (_, res) => {
208 | int foreground_pid = get_foreground_process.end (res);
209 |
210 | if (
211 | foreground_pid >= 0 &&
212 | foreground_pid != process.pid &&
213 | foreground_pid != process.foreground_pid
214 | ) {
215 | var cmdline = get_process_cmdline (foreground_pid);
216 |
217 | if (cmdline == null || cmdline == "") {
218 | // Why does this happen?
219 | debug ("Skipping process with empty cmdline");
220 | return;
221 | }
222 |
223 | process.foreground_pid = foreground_pid;
224 | process.last_foreground_task_command = cmdline;
225 | }
226 | });
227 | }
228 |
229 | // TODO: check that the main pid is still running
230 | {
231 | process.ended = !check_pid_running (process.pid);
232 | // Should we emit an event for process finished?
233 | }
234 |
235 | {
236 | if (!process.ended) {
237 | try {
238 | var source_pid = process.foreground_pid > -1
239 | ? process.foreground_pid
240 | : process.pid;
241 |
242 | var euid = get_euid_from_pid (source_pid, null);
243 |
244 | if (euid == 0) {
245 | process.context = ProcessContext.ROOT;
246 | }
247 | else {
248 | var command = get_process_cmdline (source_pid);
249 |
250 | if (command != null && command.has_prefix ("ssh")) {
251 | process.context = ProcessContext.SSH;
252 | }
253 | else {
254 | process.context = ProcessContext.DEFAULT;
255 | }
256 | }
257 | }
258 | catch (GLib.Error e) {
259 | warning (e.message);
260 | }
261 | }
262 | }
263 | }
264 | catch (GLib.Error e) {
265 | warning ("%s", e.message);
266 | }
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/src/gtk/help-overlay.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | True
5 |
6 |
7 | shortcuts
8 |
9 |
10 | Window
11 |
12 |
13 | New window
14 | app.new-window
15 |
16 |
17 |
18 |
19 | New tab
20 | win.new_tab
21 |
22 |
23 |
24 |
25 | Close tab
26 | win.close-tab
27 |
28 |
29 |
30 |
31 | Rename tab
32 | win.rename-tab
33 |
34 |
35 |
36 |
37 | Switch headbar mode
38 | win.switch-headerbar-mode
39 |
40 |
41 |
42 |
43 | Toggle fullscreen
44 | win.fullscreen
45 |
46 |
47 |
48 |
49 |
50 |
51 | Terminal
52 |
53 |
54 | Search
55 | win.search
56 |
57 |
58 |
59 |
60 | Copy
61 | win.copy
62 |
63 |
64 |
65 |
66 | Paste
67 | win.paste
68 |
69 |
70 |
71 |
72 | Zoom in
73 | win.zoom-in
74 |
75 |
76 |
77 |
78 | Zoom out
79 | win.zoom-out
80 |
81 |
82 |
83 |
84 | Reset zoom
85 | win.zoom-default
86 |
87 |
88 |
89 |
90 |
91 |
92 | Navigation
93 |
94 |
95 | Focus next tab
96 | app.focus-next-tab
97 |
98 |
99 |
100 |
101 | Focus previous tab
102 | app.focus-previous-tab
103 |
104 |
105 |
106 |
107 | Switch to Tab 1
108 | win.switch-tab-1
109 |
110 |
111 |
112 |
113 | Switch to Tab 2
114 | win.switch-tab-2
115 |
116 |
117 |
118 |
119 | Switch to Tab 3
120 | win.switch-tab-3
121 |
122 |
123 |
124 |
125 | Switch to Tab 4
126 | win.switch-tab-4
127 |
128 |
129 |
130 |
131 | Switch to Tab 5
132 | win.switch-tab-5
133 |
134 |
135 |
136 |
137 | Switch to Tab 6
138 | win.switch-tab-6
139 |
140 |
141 |
142 |
143 | Switch to Tab 7
144 | win.switch-tab-7
145 |
146 |
147 |
148 |
149 | Switch to Tab 8
150 | win.switch-tab-8
151 |
152 |
153 |
154 |
155 | Switch to Tab 9
156 | win.switch-tab-9
157 |
158 |
159 |
160 |
161 | Switch to last Tab
162 | win.switch-tab-last
163 |
164 |
165 |
166 |
167 |
168 |
169 | General
170 |
171 |
172 | Show Shortcuts
173 | win.show-help-overlay
174 |
175 |
176 |
177 |
178 | Edit Preferences
179 | win.edit_preferences
180 |
181 |
182 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.15.0 - Unreleased
4 |
5 | Features:
6 |
7 | - Added shortcut for moving tabs Shift+Ctrl+PageDown/PageUp - #225
8 | - Ctrl+PageDown/PageUp have been added as default keybindins for switching tabs,
9 | alongside (Shift)+Ctrl+Tab (yes, there are two default keybindings). You may
10 | need to reset keybindings for these two actions to see the new defaults.
11 | - The window title is now set to the title of the active tab. This is noticeable
12 | when hovering Black Box in the GNOME Overview - #317
13 | - Black Box will show a visual indicator on a tab when a command finishes in the
14 | background (similar to desktop notifications, but less noisy) - #345
15 |
16 | Improvements:
17 |
18 | - Command completion notifications have been improved. Clicking a notification
19 | will now focus the correct tab. Black Box will no longer emit two
20 | notifications for the same command. - !145 thanks to @her01n
21 |
22 | ## 0.14.0 - 2023-07-17
23 |
24 | The Sandbox Conundrum.
25 |
26 | Features:
27 |
28 | - Added new default Adwaita and Adwaita Dark color schemes - #157 thanks
29 | to @vixalien
30 | - You can now customize the working directory for new tabs. It can be set to
31 | persist the last tab's directory, the user's home directory, or an arbitrary
32 | location - #122
33 | - Closing a tab or a window that still has a running process will now prompt you
34 | for confirmation - fixes #201
35 | - Black Box now uses the default Adwaita tab style. As consequence, some header
36 | bar options, including "Show Borders" have been removed from the preferences
37 | window - #112, #253
38 | - Added the option to disable terminal bell - #106
39 | - Added the option to make bold text bright - #203
40 | - You can now get a desktop notification when a process completes on an
41 | unfocussed tab - #146
42 | - Context-aware header bar: the header bar can now have special colors when the
43 | active tab is running sudo or ssh - #239 - co-authored by @foxedb
44 | - Added open and copy link options to the right-click menu - #141
45 | - You can now rename tabs with the new tab right-click menu, or with a new
46 | shortcut `Shift + Control + R` - #242
47 | - Added a quick application style switcher to the window menu - #147
48 |
49 | Improvements:
50 |
51 | - Some configuration options have been grouped together in the preferences
52 | window - #254
53 | - Application title is now bold when there's a single tab open - #235
54 | - Performance and bundle size optimizations - #283, #284
55 | - Black Box now has more Flatpak permissions to overcome errors reported by
56 | users - #186, #215
57 |
58 | Bug fixes:
59 |
60 | - Fixed an issue that caused terminals not to be destroyed when their tabs were
61 | closed - #261
62 | - The window title is now centered when there's only one tab - #199
63 | - Improved keybinding validation, allowing more valid key combinations to be
64 | used - #245
65 | - Sixel is now disabled for VTE builds that don't support it. This primarily
66 | affects non-Flatpak users, as all Flatpak builds ship VTE with Sixel
67 | support - #273
68 | - Fixed an issue that caused windows launched with custom commands to not have a
69 | title - #237
70 | - Black Box will now show an error banner if spawning a shell or custom
71 | command failed and will no longer close immediately - #97, #121, #259
72 |
73 | ## 0.13.2 - 2023-01-19
74 |
75 | Second 0.13 patch release.
76 |
77 | Features:
78 |
79 | - Added support for setting multiple shortcuts for the same action - #212
80 | - You can now reset one, or all custom shortcuts back to default - #211
81 | - A warning is displayed if a user selects "Unlimited" scrollback mode - #228
82 |
83 | Bug fixes:
84 |
85 | - Added workaround for a Vala error that would cause Black Box to crash
86 |
87 | ## 0.13.1 - 2023-01-16
88 |
89 | First 0.13 patch release.
90 |
91 | Features:
92 |
93 | - New Scrollback Mode allows you to set scrollback to a fixed number of lines,
94 | unlimited lines, or disable scrollback altogether - #197
95 | - Allow setting font style (regular, light, bold, etc) - #170
96 |
97 | Improvements:
98 |
99 | - Updated French, Italian, and Turkish translations
100 |
101 | Bug fixes:
102 |
103 | - Added missing "Open Preferences" shortcut to help overlay - @sabriunal
104 | - Header bar and tabs are now properly colored when the app is unfocussed
105 | - Fixed regression in window border color when "Show Borders" is enabled
106 | - Window border is no longer displayed when Black Box is docked left, right, or
107 | maximized #181
108 | - Improved keybinding validation, allowing more valid key combinations to be
109 | used - #214
110 | - Tab navigation shortcuts now work as expected - #217
111 | - Fixed default "Reset Zoom" keybinding
112 | - Fixed issue that prevented development builds of Black Box from running when
113 | installed via Flatpak - #210
114 |
115 | ## 0.13.0 - 2023-01-13
116 |
117 | The latest version of Black Box brings much-awaited new features and bug fixes.
118 |
119 | Features:
120 |
121 | - Customizable keyboard shortcuts
122 | - Background transparency - thanks to @bennyp
123 | - Customizable cursor blinking mode - thanks to @knuxify
124 | - Experimental Sixel support - thanks to @PJungkamp
125 |
126 | Bug fixes:
127 |
128 | - Manually set VTE_VERSION environment variable - fixes compatibility with a few terminal programs - #208
129 | - Copying text outside the current scroll view now works correctly - #166
130 | - Scrolling with a touchpad or touchscreen now works as intended - #179
131 |
132 | ## 0.12.2 - 2022.11.16
133 |
134 | Features:
135 |
136 | - Added Turkish translation - thanks to @sabriunal
137 |
138 | Improvements:
139 |
140 | - UI consistency - thanks to @sabriunal
141 | - Clear selection after copying text with easy copy/paste - thanks to @1player
142 |
143 | Bug fixes:
144 |
145 | - Text selection was broken - #177
146 |
147 | ## 0.12.1 - 2022.09.28
148 |
149 | Features:
150 |
151 | - Added Brazilian Portuguese translation - thanks to @ciro-mota
152 |
153 | Improvements:
154 |
155 | - Updated French, Russian, Italian, Czech, and Swedish translations
156 |
157 | Bug fixes:
158 |
159 | - Flatpak CLI `1.13>=` had weird output - #165
160 |
161 | ## 0.12.0 - 2022.08.16
162 |
163 | Features:
164 |
165 | - Added support for searching text from terminal output - #93
166 | - Open a new tab by clicking on the header bar with the middle mouse button - #88
167 | - Customizable number of lines to keep buffered - #92
168 | - Added option to reserve an area in the header bar to drag the window
169 | - Added Spanish translation - thanks @oscfdezdz
170 |
171 | Improvements:
172 |
173 | - Greatly improved performance, thanks to an update in VTE
174 | - Theme integration now uses red, green, blue, and yellow from your terminal
175 | theme to paint the rest of the app
176 | - Theme integration now uses a different approach to calculate colors based on
177 | your terminal theme's background color. This results in more aesthetically
178 | pleasing header bar colors
179 |
180 | Bug fixes:
181 |
182 | - The primary clipboard now works as intended - #46
183 | - The "Reset Preferences" button is now translatable - #117
184 | - High CPU usage - #21
185 | - Fix right-click menu spawn position - closes #52
186 | - Fix long loading times - fixes #135
187 |
188 | ## 0.11.3 - 2022.07.21
189 |
190 | - Ctrl + click can now be used to open URLs - #25
191 |
192 | ## 0.11.2 - 2022.07.17
193 |
194 | - Updated translations
195 | - Added Simplified Chinese translation
196 | - Black Box now sets the COLORTERM env variable to `truecolor` - #98
197 |
198 | ## 0.11.1 - 2022.07.13
199 |
200 | Features:
201 |
202 | - Black Box will set the BLACKBOX_THEMES_DIR env variable to the user's theme
203 | folder - #82
204 |
205 | Bug fixes:
206 |
207 | - Fix opaque floating header bar
208 | - User themes dir is no longer hard-coded and will be different for host vs
209 | Flatpak - #90 thanks @nahuelwexd
210 |
211 | ## 0.11.0 - 2022.07.13
212 |
213 | Features:
214 |
215 | - The preferences window has a new layout that allows for more
216 | features/customization to be added
217 | - Added support for the system-wide dark style preference - #17
218 | - Users can now set a terminal color scheme for dark style and another for light
219 | style
220 | - Black Box now uses the new libadwaita about window
221 | - New themes included with Black Box: one-dark, pencil-dark, pencil-light,
222 | tomorrow, and tommorrow-night
223 | - Black Box will also load themes from `~/.var/app/com.raggesilver.BlackBox/schemes` - #54
224 | - You can customize which and how your shell is spawned in Black Box - #43
225 | - Run command as login shell
226 | - Set custom command instead of the default shell
227 |
228 | Deprecations:
229 |
230 | - The Linux and Tango color schemes have been removed
231 | - All color schemes must now set `background-color` and `foreground-color`
232 |
233 | Bug fixes:
234 |
235 | - Fixed a bug that prevented users from typing values in the preferences window - #13
236 | - Middle-click paste will now paste from user selection - #46
237 | - Color scheme sorting is now case insensitive
238 | - Long window title resizes window in single tab mode - #77
239 | - Drag-n-drop now works with multiple files - #67
240 | - Improved theme integration. Popovers, menus, and lists are now properly styled
241 | according to the user's terminal color scheme - #42
242 |
243 | ## 0.10.1 - 2022.07.08
244 |
245 | Features:
246 |
247 | - Improved German translation - thanks @konstantin.tch
248 | - Added Czech translation - thanks @panmourovaty
249 | - Added Russian translation - thanks @acephale
250 | - Added Swedish translation - thanks @droidbittin
251 |
252 | Bug fixes:
253 |
254 | - Black Box now sets the TERM_PROGRAM env variable. This makes apps like
255 | neofetch report a correct terminal app in Flatpak - #53
256 | - "Remember window size" will now remember fullscreen and maximized state too - #55
257 |
258 | ## 0.10.0 - 2022.07.04
259 |
260 | Features:
261 |
262 | - New single tab mode makes it easier to drag the window and the UI more
263 | aesthetically pleasing when there's a single tab open - #31
264 | - Added middle-click paste (only if enabled system-wide) - #46
265 | - Added French translation - thanks @rene-coty
266 | - Added Dutch translation - thanks @Vistaus
267 | - Added German translation - thanks @ktutsch
268 |
269 | Bug fixes:
270 |
271 | - Buttons in headerbar are no longer focusable - #49
272 | - Labels and titles in preferences window now follow GNOME HIG for typography -
273 | !21 thanks @TheEvilSkeleton
274 | - Disable unimplemented `app.quit` accelerator - #44
275 |
276 | ## 0.9.1 - 2022.07.02
277 |
278 | Use patched VTE to enable copying.
279 |
280 | ## 0.9.0 - 2022.07.01
281 |
282 | Features:
283 |
284 | - Added cell spacing option #36
285 | - i18n support #27 - thanks @yilozt
286 |
287 | Bug fixes:
288 |
289 | - Fixed floating controls action row cannot be activated (!19) - thanks @TheEvilSkeleton
290 | - New custom headerbar fixes unwanted spacing with controls on left side #38
291 | - Flathub builds will no longer have "striped headerbar" #40
292 | - A button is now displayed in the headerbar to leave fullscreen #39
293 |
--------------------------------------------------------------------------------
/src/widgets/ShortcutEditor.vala:
--------------------------------------------------------------------------------
1 | /* ShortcutEditor.vala
2 | *
3 | * Copyright 2022 Paulo Queiroz
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | *
18 | * SPDX-License-Identifier: GPL-3.0-or-later
19 | */
20 |
21 | class Terminal.Action : Object {
22 | public string name;
23 | public string? label;
24 | public string[] accelerators;
25 |
26 | public Action () {
27 | Object ();
28 | }
29 | }
30 |
31 | [GtkTemplate (ui = "/com/raggesilver/BlackBox/gtk/shortcut-row.ui")]
32 | class Terminal.ShortcutRow : Adw.ActionRow {
33 |
34 | public Action? action { get; set; default = null; }
35 |
36 | [GtkChild] unowned Gtk.Box accelerators_box;
37 | // [GtkChild] unowned Gtk.MenuButton menu_button;
38 | [GtkChild] unowned Gtk.PopoverMenu popover;
39 |
40 | construct {
41 | this.notify ["action"].connect (this.update_ui);
42 | }
43 |
44 | public void update_ui () {
45 | this.title = this.action?.label ?? this.action?.name ?? "";
46 |
47 | // Remove previous ShortcutLabels
48 | {
49 | var c = this.accelerators_box.get_first_child ();
50 | while (c != null) {
51 | this.accelerators_box.remove (c);
52 | c = c.get_next_sibling ();
53 | }
54 | }
55 |
56 | {
57 | var menu = new Menu ();
58 |
59 | var mi = new MenuItem (_("Add Keybinding"), null);
60 | mi.set_action_and_target_value (
61 | ACTION_SHORTCUT_EDITOR_ADD_KEYBINDING,
62 | this.action.name
63 | );
64 | menu.append_item (mi);
65 |
66 | mi = new MenuItem (_("Reset Keybindings"), null);
67 | mi.set_action_and_target_value (
68 | ACTION_SHORTCUT_EDITOR_RESET,
69 | this.action.name
70 | );
71 | menu.append_item (mi);
72 |
73 | var keymap = Keymap.get_default ();
74 | var accels = keymap.get_accelerators_for_action (this.action.name);
75 |
76 | if (accels != null) {
77 | var section = new Menu ();
78 | foreach (unowned string accel in accels) {
79 | if (accel != null) {
80 | mi = new MenuItem (
81 | _("Remove %s").printf (get_accel_as_label (accel)),
82 | null
83 | );
84 | mi.set_action_and_target_value (
85 | ACTION_SHORTCUT_EDITOR_REMOVE_KEYBINDING,
86 | accel
87 | );
88 | section.append_item (mi);
89 | }
90 | }
91 | if (section.get_n_items () > 0) {
92 | menu.append_section (null, section);
93 | }
94 | }
95 |
96 | this.popover.set_menu_model (menu);
97 | }
98 |
99 | if (
100 | this.action != null &&
101 | (
102 | this.action.accelerators.length == 0 ||
103 | this.action.accelerators [0] == null
104 | )
105 | ) {
106 | this.accelerators_box.append (new Gtk.Label (_("Disabled")) {
107 | css_classes = { "dim-label" },
108 | });
109 | } else {
110 | foreach (unowned string accel in this.action.accelerators) {
111 | this.accelerators_box.append (new Gtk.ShortcutLabel (accel) {
112 | halign = Gtk.Align.END,
113 | });
114 | }
115 | }
116 | }
117 | }
118 |
119 | [GtkTemplate (ui = "/com/raggesilver/BlackBox/gtk/shortcut-editor.ui")]
120 | public class Terminal.ShortcutEditor : Adw.PreferencesPage {
121 | public Gtk.Window window { get; set; }
122 | public Gtk.Application app { get; set; }
123 |
124 | [GtkChild] unowned Gtk.ListBox list_box;
125 |
126 | static Gee.HashMap action_map;
127 |
128 | ListStore store = new ListStore (typeof (Action));
129 |
130 | static construct {
131 | action_map = new Gee.HashMap ();
132 |
133 | action_map.@set (ACTION_FOCUS_NEXT_TAB, _("Focus Next Tab"));
134 | action_map.@set (ACTION_FOCUS_PREVIOUS_TAB, _("Focus Previous Tab"));
135 | action_map.@set (ACTION_NEW_WINDOW, _("New Window"));
136 | action_map.@set (ACTION_WIN_SWITCH_HEADER_BAR_MODE, _("Toggle Header Bar"));
137 | action_map.@set (ACTION_WIN_NEW_TAB, _("New Tab"));
138 | action_map.@set (ACTION_WIN_EDIT_PREFERENCES, _("Preferences"));
139 | action_map.@set (ACTION_WIN_COPY, _("Copy"));
140 | action_map.@set (ACTION_WIN_PASTE, _("Paste"));
141 | action_map.@set (ACTION_WIN_SEARCH, _("Search"));
142 | action_map.@set (ACTION_WIN_FULLSCREEN, _("Fullscreen"));
143 | action_map.@set (ACTION_WIN_SHOW_HELP_OVERLAY, _("Keyboard Shortcuts"));
144 | action_map.@set (ACTION_WIN_ZOOM_IN, _("Zoom In"));
145 | action_map.@set (ACTION_WIN_ZOOM_OUT, _("Zoom Out"));
146 | action_map.@set (ACTION_WIN_ZOOM_DEFAULT, _("Reset Zoom"));
147 | action_map.@set (ACTION_WIN_CLOSE_TAB, _("Close Tab"));
148 | action_map.@set (ACTION_WIN_RENAME_TAB, _("Rename Tab"));
149 | action_map.@set (ACTION_WIN_MOVE_TAB_LEFT, _("Move Tab Left"));
150 | action_map.@set (ACTION_WIN_MOVE_TAB_RIGHT, _("Move Tab Right"));
151 |
152 | action_map.@set (ACTION_WIN_SWITCH_TAB_1, _("Switch to Tab %u").printf (1));
153 | action_map.@set (ACTION_WIN_SWITCH_TAB_2, _("Switch to Tab %u").printf (2));
154 | action_map.@set (ACTION_WIN_SWITCH_TAB_3, _("Switch to Tab %u").printf (3));
155 | action_map.@set (ACTION_WIN_SWITCH_TAB_4, _("Switch to Tab %u").printf (4));
156 | action_map.@set (ACTION_WIN_SWITCH_TAB_5, _("Switch to Tab %u").printf (5));
157 | action_map.@set (ACTION_WIN_SWITCH_TAB_6, _("Switch to Tab %u").printf (6));
158 | action_map.@set (ACTION_WIN_SWITCH_TAB_7, _("Switch to Tab %u").printf (7));
159 | action_map.@set (ACTION_WIN_SWITCH_TAB_8, _("Switch to Tab %u").printf (8));
160 | action_map.@set (ACTION_WIN_SWITCH_TAB_9, _("Switch to Tab %u").printf (9));
161 | action_map.@set (ACTION_WIN_SWITCH_TAB_LAST, _("Switch to Last Tab"));
162 | }
163 |
164 | construct {
165 | this.build_ui ();
166 |
167 | this.list_box.margin_bottom = 32;
168 |
169 | this.install_action (
170 | ACTION_SHORTCUT_EDITOR_RESET,
171 | "s",
172 | (Gtk.WidgetActionActivateFunc) on_shortcut_editor_reset
173 | );
174 |
175 | this.install_action (
176 | ACTION_SHORTCUT_EDITOR_RESET_ALL,
177 | null,
178 | (Gtk.WidgetActionActivateFunc) on_shortcut_editor_reset_all
179 | );
180 |
181 | this.install_action (
182 | ACTION_SHORTCUT_EDITOR_REMOVE_KEYBINDING,
183 | "s",
184 | (Gtk.WidgetActionActivateFunc) on_shortcut_editor_remove_keybinding
185 | );
186 |
187 | this.install_action (
188 | ACTION_SHORTCUT_EDITOR_ADD_KEYBINDING,
189 | "s",
190 | (Gtk.WidgetActionActivateFunc) on_shortcut_editor_add_keybinding
191 | );
192 | }
193 |
194 | void on_shortcut_editor_add_keybinding (string _, Variant action) {
195 | var action_name = action.get_string ();
196 | var keymap = Keymap.get_default ();
197 |
198 | var w = new ShortcutDialog () {
199 | shortcut_name = action_map [action_name] ?? action_name,
200 | };
201 |
202 | string? new_accel = null;
203 |
204 | w.shortcut_set.connect ((_new_accel) => {
205 | new_accel = _new_accel;
206 | });
207 |
208 | w.response.connect ((response) => {
209 | if (response == Gtk.ResponseType.APPLY) {
210 | keymap.add_shortcut_for_action.begin (
211 | action_name,
212 | new_accel,
213 | (o, res) => {
214 | if (keymap.add_shortcut_for_action.end (res)) {
215 | this.apply_save_and_refresh ();
216 | }
217 | }
218 | );
219 | }
220 | });
221 |
222 | w.present (this.window);
223 | }
224 |
225 | void on_shortcut_editor_remove_keybinding (string _, Variant _accel) {
226 | var keymap = Keymap.get_default ();
227 |
228 | var accel = _accel.get_string ();
229 | var action = keymap.get_action_for_shortcut (accel);
230 |
231 | if (action != null) {
232 | keymap.remove_shortcut_from_action (action, accel);
233 | this.apply_save_and_refresh ();
234 | }
235 | }
236 |
237 | void on_shortcut_editor_reset_all () {
238 | this.request_reset_all.begin ();
239 | }
240 |
241 | async void request_reset_all () {
242 | Adw.AlertDialog dlg = new Adw.AlertDialog (
243 | _("Reset All Shortcuts?"),
244 | _("This will reset all shortcuts to default and overwrite your config file. This action is irreversible."));
245 |
246 | dlg.add_response ("cancel", _("Cancel"));
247 | dlg.add_response ("reset", _("Reset"));
248 | dlg.set_close_response ("cancel");
249 | dlg.set_response_appearance ("reset", Adw.ResponseAppearance.DESTRUCTIVE);
250 |
251 | dlg.response.connect ((response) => {
252 | if (response == "reset") {
253 | var keymap = Keymap.get_default ();
254 | keymap.reset_user_keymap ();
255 | this.apply_save_and_refresh ();
256 | }
257 | });
258 |
259 | dlg.present (this.window);
260 | }
261 |
262 | void on_shortcut_editor_reset (string _action_name, Variant shortcut_name) {
263 | this.request_shortcut_reset.begin (shortcut_name.get_string ());
264 | }
265 |
266 | async void request_shortcut_reset (string action_name) {
267 | var keymap = Keymap.get_default ();
268 | yield keymap.reset_shortcuts_for_action (action_name);
269 | this.apply_save_and_refresh ();
270 | }
271 |
272 | void apply_save_and_refresh () {
273 | var keymap = Keymap.get_default ();
274 | keymap.save ();
275 | keymap.apply (this.app);
276 | this.populate_list ();
277 | }
278 |
279 | void build_ui () {
280 | this.list_box.bind_model (
281 | this.store,
282 | (_action) => {
283 | return new ShortcutRow () {
284 | action = _action as Action,
285 | };
286 | }
287 | );
288 |
289 | this.populate_list ();
290 | }
291 |
292 | void populate_list (bool clear = true) {
293 | if (clear) {
294 | this.store.remove_all ();
295 | }
296 |
297 | var keymap = Keymap.get_default ();
298 | foreach (string action in keymap.keymap.get_keys ()) {
299 | var a = new Action ();
300 |
301 | a.name = action;
302 | a.label = action_map [action];
303 | a.accelerators = keymap.get_accelerators_for_action (action);
304 |
305 | this.store.insert_sorted (a, (CompareDataFunc) store_stort_func);
306 | }
307 | }
308 |
309 | static int store_stort_func (Action action_a, Action action_b) {
310 | if (action_a.label != null && action_b != null) {
311 | return strcmp (action_a.label, action_b.label);
312 | }
313 | return strcmp (action_a.name, action_b.name);
314 | }
315 | }
316 |
--------------------------------------------------------------------------------