├── po ├── .gitignore ├── meson.build ├── LINGUAS └── POTFILES ├── src ├── style-dark.css ├── services │ ├── meson.build │ ├── Scheme.vala │ ├── Settings.vala │ └── ProcessWatcher.vala ├── utils │ ├── meson.build │ ├── Shortcuts.vala │ ├── Css.vala │ ├── Widget.vala │ ├── File.vala │ ├── Constants.vala │ ├── Dialogs.vala │ └── SettingsBinder.vala ├── icons │ └── scalable │ │ └── actions │ │ └── checkmark-small-symbolic.svg ├── vapi │ ├── config.vapi │ └── libpcre2-8.vapi ├── widgets │ ├── meson.build │ ├── StyleSwitcher.vala │ ├── HeaderBar.vala │ ├── ShortcutDialog.vala │ ├── AboutDialog.vala │ ├── TerminalTab.vala │ ├── SearchToolbar.vala │ ├── ColorSchemeThumbnail.vala │ └── ShortcutEditor.vala ├── config.vala.in ├── blackbox.gresource.xml ├── main.vala ├── blackbox-link-system-fonts ├── gtk │ ├── terminal-tab.ui │ ├── tab-menu.ui │ ├── shortcut-row.ui │ ├── shortcut-editor.ui │ ├── shortcut-dialog.ui │ ├── terminal-menu.ui │ ├── style-switcher.ui │ ├── search-toolbar.ui │ └── help-overlay.ui ├── style.css ├── meson.build ├── CommandLine.vala ├── Application.vala └── resources │ └── svg │ └── color-scheme-thumbnail.svg ├── meson_options.txt ├── .editorconfig ├── .gitignore ├── .gitlab ├── issue_templates │ ├── feature_request.md │ └── bug.md └── merge_request_templates │ └── release.md ├── data ├── icons │ ├── hicolor │ │ └── scalable │ │ │ ├── actions │ │ │ ├── checkmark-small-symbolic.svg │ │ │ ├── external-link-symbolic.svg │ │ │ ├── settings-symbolic.svg │ │ │ ├── com.raggesilver.BlackBox-show-headerbar-symbolic.svg │ │ │ └── com.raggesilver.BlackBox-fullscreen-symbolic.svg │ │ │ └── apps │ │ │ └── com.raggesilver.BlackBox.svg │ └── meson.build ├── com.raggesilver.BlackBox.desktop.in ├── schemes │ ├── adwaita-light.json │ ├── tomorrow.json │ ├── pencil-dark.json │ ├── pencil-light.json │ ├── adwaita-dark.json │ ├── japanesque.json │ ├── material.json │ ├── one-dark.json │ ├── yaru.json │ ├── monokai.json │ ├── dracula.json │ ├── solarized-dark.json │ ├── solarized-light.json │ ├── tommorow-night.json │ ├── gruvbox-dark.json │ ├── dracula-light.json │ ├── gruvbox-light.json │ ├── base16-twilight-dark.json │ └── orchis.json ├── meson.build └── com.raggesilver.BlackBox.gschema.xml ├── toolbox ├── meson.build └── main.c ├── meson.build ├── com.raggesilver.BlackBox.json ├── .gitlab-ci.yml ├── CONTRIBUTING.md ├── README.md └── CHANGELOG.md /po/.gitignore: -------------------------------------------------------------------------------- 1 | !blackbox.pot 2 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('blackbox', preset: 'glib') 2 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | cs 2 | de 3 | es 4 | fr 5 | it 6 | nl 7 | ru 8 | sv 9 | tr 10 | zh_CN 11 | pt_BR 12 | ja 13 | pl 14 | pt 15 | -------------------------------------------------------------------------------- /src/style-dark.css: -------------------------------------------------------------------------------- 1 | @define-color root_context_color shade(@red_5, 0.7); 2 | @define-color ssh_context_color shade(@purple_5, 0.7); 3 | -------------------------------------------------------------------------------- /src/services/meson.build: -------------------------------------------------------------------------------- 1 | blackbox_sources += files([ 2 | 'ProcessWatcher.vala', 3 | 'Scheme.vala', 4 | 'Settings.vala', 5 | 'Shortcuts.vala', 6 | 'ThemeProvider.vala', 7 | ]) 8 | -------------------------------------------------------------------------------- /src/utils/meson.build: -------------------------------------------------------------------------------- 1 | blackbox_sources += files( 2 | 'Constants.vala', 3 | 'Css.vala', 4 | 'Dialogs.vala', 5 | 'File.vala', 6 | 'SettingsBinder.vala', 7 | 'Shortcuts.vala', 8 | 'Terminal.vala', 9 | 'Widget.vala', 10 | ) 11 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('devel', type: 'boolean', value: false) 2 | option('network_tests', type: 'boolean', value: false) 3 | option('blackbox_debug_memory', type: 'boolean', value: false) 4 | option('blackbox_is_flatpak', type: 'boolean', value: true) 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{ui,xml}] 11 | indent_size = 2 12 | 13 | [Makefile] 14 | indent_style = tab 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Trash 2 | *.*~ 3 | \#*# 4 | *.swp 5 | 6 | # Test files 7 | test* 8 | mocks/ 9 | 10 | # Build dirs/files 11 | app/ 12 | app_build/ 13 | _build/ 14 | .flatpak-builder/ 15 | repo/ 16 | *.flatpak 17 | .buildconfig 18 | 19 | # IDE dirs 20 | .vscode/ 21 | .idea/ 22 | 23 | # i18n 24 | po/*.pot 25 | -------------------------------------------------------------------------------- /src/icons/scalable/actions/checkmark-small-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.gitlab/issue_templates/feature_request.md: -------------------------------------------------------------------------------- 1 | ## Feature Request 2 | 3 | Describe the feature request and your use case for it 4 | 5 | ## References 6 | 7 | Is this feature available in other apps? 8 | 9 | ## Video or Screenshot 10 | 11 | If possible, please attach a video or screenshot of this feature in other apps 12 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/checkmark-small-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /toolbox/meson.build: -------------------------------------------------------------------------------- 1 | toolbox_sources = files( 2 | 'main.c', 3 | ) 4 | 5 | toolbox_deps = [] 6 | 7 | toolbox_build_args = ['-Werror', '-Wextra', '-Wall'] 8 | 9 | executable('terminal-toolbox', 10 | toolbox_sources, 11 | c_args: toolbox_build_args, 12 | dependencies: toolbox_deps, 13 | install: true, 14 | ) 15 | -------------------------------------------------------------------------------- /src/vapi/config.vapi: -------------------------------------------------------------------------------- 1 | namespace Terminal { 2 | public const string APP_NAME; 3 | public const string APP_CLI_NAME; 4 | public const string GETTEXT_PACKAGE; 5 | public const string DATADIR; 6 | public const string LOCALEDIR; 7 | public const string VERSION; 8 | public const string APP_ID; 9 | public const bool DEVEL; 10 | } 11 | -------------------------------------------------------------------------------- /data/com.raggesilver.BlackBox.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Black Box 3 | Exec=blackbox 4 | Terminal=false 5 | Type=Application 6 | Categories=GNOME;GTK;System;TerminalEmulator; 7 | StartupNotify=false 8 | Keywords=Terminal;terminal;shell;prompt;command;commandline;cmd;box; 9 | Icon=com.raggesilver.BlackBox 10 | X-GNOME-UsesNotifications=true 11 | -------------------------------------------------------------------------------- /src/widgets/meson.build: -------------------------------------------------------------------------------- 1 | blackbox_sources += files( 2 | 'AboutDialog.vala', 3 | 'ColorSchemeThumbnail.vala', 4 | 'HeaderBar.vala', 5 | 'SearchToolbar.vala', 6 | 'ShortcutDialog.vala', 7 | 'ShortcutEditor.vala', 8 | 'PreferencesWindow.vala', 9 | 'StyleSwitcher.vala', 10 | 'Terminal.vala', 11 | 'TerminalTab.vala', 12 | 'Window.vala', 13 | ) 14 | -------------------------------------------------------------------------------- /.gitlab/merge_request_templates/release.md: -------------------------------------------------------------------------------- 1 | # Release X.XX.X 2 | 3 | Black Box release checklist: 4 | 5 | - [ ] Release notes 6 | - [ ] Added to AboutWindow 7 | - [ ] Added to com.raggesilver.BlackBox.appdata.xml.in 8 | - [ ] Added to CHANGELOG.md 9 | - [ ] Version was bumped 10 | - [ ] Version does not end in `-alpha`, `-rc.X`, or other preview suffix 11 | - [ ] This version has a milestone 12 | -------------------------------------------------------------------------------- /src/config.vala.in: -------------------------------------------------------------------------------- 1 | namespace Terminal { 2 | public const string APP_NAME = "BlackBox"; 3 | public const string APP_CLI_NAME = @PROJECT_NAME@; 4 | public const string GETTEXT_PACKAGE = @GETTEXT_PACKAGE@; 5 | public const string DATADIR = @DATADIR@; 6 | public const string LOCALEDIR = @LOCALEDIR@; 7 | public const string VERSION = @VERSION@; 8 | public const string APP_ID = "com.raggesilver.BlackBox"; 9 | public const bool DEVEL = @DEVEL@; 10 | } 11 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('blackbox', ['c', 'vala'], 2 | version: '0.14.0', 3 | meson_version: '>= 0.50.0', 4 | default_options: [ 'warning_level=2', 5 | ], 6 | ) 7 | 8 | i18n = import('i18n') 9 | 10 | add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (meson.project_name()), language:'c') 11 | 12 | subdir('data') 13 | subdir('src') 14 | subdir('po') 15 | 16 | if get_option('blackbox_is_flatpak') 17 | subdir('toolbox') 18 | endif 19 | 20 | meson.add_install_script('build-aux/meson/postinstall.py') 21 | -------------------------------------------------------------------------------- /data/schemes/adwaita-light.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Adwaita", 3 | "comment": "Adwaita color scheme", 4 | "foreground-color": "#000000", 5 | "background-color": "#ffffff", 6 | "use-theme-colors": true, 7 | "palette": [ 8 | "#241F31", 9 | "#C01C28", 10 | "#2EC27E", 11 | "#F5C211", 12 | "#1E78E4", 13 | "#9841BB", 14 | "#0AB9DC", 15 | "#C0BFBC", 16 | "#5E5C64", 17 | "#ED333B", 18 | "#57E389", 19 | "#F8E45C", 20 | "#51A1FF", 21 | "#C061CB", 22 | "#4FD2FD", 23 | "#F6F5F4" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/tomorrow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tomorrow", 3 | "comment": "Ported for Terminix Colour Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#4d4d4c", 6 | "background-color": "#ffffff", 7 | "palette": [ 8 | "#000000", 9 | "#c82829", 10 | "#718c00", 11 | "#eab700", 12 | "#4271ae", 13 | "#8959a8", 14 | "#3e999f", 15 | "#ffffff", 16 | "#000000", 17 | "#c82829", 18 | "#718c00", 19 | "#eab700", 20 | "#4271ae", 21 | "#8959a8", 22 | "#3e999f", 23 | "#ffffff" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/pencil-dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pencil Dark", 3 | "comment": "Ported for Terminix Colour Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#f1f1f1", 6 | "background-color": "#212121", 7 | "palette": [ 8 | "#212121", 9 | "#c30771", 10 | "#10a778", 11 | "#a89c14", 12 | "#008ec4", 13 | "#523c79", 14 | "#20a5ba", 15 | "#d9d9d9", 16 | "#424242", 17 | "#fb007a", 18 | "#5fd7af", 19 | "#f3e430", 20 | "#20bbfc", 21 | "#6855de", 22 | "#4fb8cc", 23 | "#f1f1f1" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/pencil-light.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pencil Light", 3 | "comment": "Ported for Terminix Colour Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#424242", 6 | "background-color": "#f1f1f1", 7 | "palette": [ 8 | "#212121", 9 | "#c30771", 10 | "#10a778", 11 | "#a89c14", 12 | "#008ec4", 13 | "#523c79", 14 | "#20a5ba", 15 | "#d9d9d9", 16 | "#424242", 17 | "#fb007a", 18 | "#5fd7af", 19 | "#f3e430", 20 | "#20bbfc", 21 | "#6855de", 22 | "#4fb8cc", 23 | "#f1f1f1" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/adwaita-dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Adwaita Dark", 3 | "comment": "Adwaita dark color scheme", 4 | "foreground-color": "#ffffff", 5 | "background-color": "#1E1E1E", 6 | "use-theme-colors": true, 7 | "palette": [ 8 | "#241F31", 9 | "#C01C28", 10 | "#2EC27E", 11 | "#F5C211", 12 | "#1E78E4", 13 | "#9841BB", 14 | "#0AB9DC", 15 | "#C0BFBC", 16 | "#5E5C64", 17 | "#ED333B", 18 | "#57E389", 19 | "#F8E45C", 20 | "#51A1FF", 21 | "#C061CB", 22 | "#4FD2FD", 23 | "#F6F5F4" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/japanesque.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Japanesque", 3 | "comment": "Ported for Terminix Colour Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#f7f6ec", 6 | "background-color": "#1e1e1e", 7 | "palette": [ 8 | "#343935", 9 | "#cf3f61", 10 | "#7bb75b", 11 | "#e9b32a", 12 | "#4c9ad4", 13 | "#a57fc4", 14 | "#389aad", 15 | "#fafaf6", 16 | "#595b59", 17 | "#d18fa6", 18 | "#767f2c", 19 | "#78592f", 20 | "#135979", 21 | "#604291", 22 | "#76bbca", 23 | "#b2b5ae" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/external-link-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/schemes/material.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Material", 3 | "comment": "Material Oceanic Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#A1B0B8", 6 | "background-color": "#263238", 7 | "palette": [ 8 | "#252525", 9 | "#FF5252", 10 | "#C3D82C", 11 | "#FFC135", 12 | "#42A5F5", 13 | "#D81B60", 14 | "#00ACC1", 15 | "#F5F5F5", 16 | "#708284", 17 | "#FF5252", 18 | "#C3D82C", 19 | "#FFC135", 20 | "#42A5F5", 21 | "#D81B60", 22 | "#00ACC1", 23 | "#F5F5F5" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/one-dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "One Dark", 3 | "comment": "Ported for Terminix Colour Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#abb2bf", 6 | "background-color": "#282c34", 7 | "palette": [ 8 | "#000000", 9 | "#E06C75", 10 | "#98c379", 11 | "#d19a66", 12 | "#61aeee", 13 | "#c678dd", 14 | "#56b6c2", 15 | "#abb2bf", 16 | "#5c6370", 17 | "#e06c75", 18 | "#98c379", 19 | "#d19a66", 20 | "#62afee", 21 | "#c678dd", 22 | "#56b6c2", 23 | "#ffffff"] 24 | } 25 | -------------------------------------------------------------------------------- /data/schemes/yaru.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yaru", 3 | "comment": "Yaru Color Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#ffffff", 6 | "background-color": "#300a24", 7 | "palette": [ 8 | "#2E3436", 9 | "#CC0000", 10 | "#4E9A06", 11 | "#C4A000", 12 | "#3465A4", 13 | "#75507B", 14 | "#06989A", 15 | "#D3D7CF", 16 | "#555753", 17 | "#EF2929", 18 | "#8AE234", 19 | "#FCE94F", 20 | "#729FCF", 21 | "#AD7FA8", 22 | "#34E2E2", 23 | "#EEEEEC" 24 | ] 25 | } -------------------------------------------------------------------------------- /data/schemes/monokai.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Monokai Dark", 3 | "comment": "Monokai Dark Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#F8F8F2", 6 | "background-color": "#232323", 7 | "palette": [ 8 | "#232323", 9 | "#f92672", 10 | "#a6e22e", 11 | "#f4bf75", 12 | "#66d9ef", 13 | "#ae81ff", 14 | "#a1efe4", 15 | "#f8f8f2", 16 | "#75715e", 17 | "#f92672", 18 | "#a6e22e", 19 | "#f4bf75", 20 | "#66d9ef", 21 | "#ae81ff", 22 | "#a1efe4", 23 | "#f9f8f5" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/dracula.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dracula", 3 | "comment": "Ported for Terminix Colour Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#f8f8f2", 6 | "background-color": "#1e1f29", 7 | "palette": [ 8 | "#000000", 9 | "#ff5555", 10 | "#50fa7b", 11 | "#f1fa8c", 12 | "#bd93f9", 13 | "#ff79c6", 14 | "#8be9fd", 15 | "#bbbbbb", 16 | "#555555", 17 | "#ff5555", 18 | "#50fa7b", 19 | "#f1fa8c", 20 | "#bd93f9", 21 | "#ff79c6", 22 | "#8be9fd", 23 | "#ffffff" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/solarized-dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Solarized Dark", 3 | "comment": "Taken from Gnome Terminal", 4 | "use-theme-colors": false, 5 | "foreground-color": "#839496", 6 | "background-color": "#002B36", 7 | "palette": [ 8 | "#073642", 9 | "#DC322F", 10 | "#859900", 11 | "#B58900", 12 | "#268BD2", 13 | "#D33682", 14 | "#2AA198", 15 | "#EEE8D5", 16 | "#002B36", 17 | "#CB4B16", 18 | "#586E75", 19 | "#657B83", 20 | "#839496", 21 | "#6C71C4", 22 | "#93A1A1", 23 | "#FDF6E3" 24 | ] 25 | } -------------------------------------------------------------------------------- /data/schemes/solarized-light.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Solarized Light", 3 | "comment": "Taken from Gnome Terminal", 4 | "use-theme-colors": false, 5 | "foreground-color": "#657B83", 6 | "background-color": "#FDF6E3", 7 | "palette": [ 8 | "#073642", 9 | "#DC322F", 10 | "#859900", 11 | "#B58900", 12 | "#268BD2", 13 | "#D33682", 14 | "#2AA198", 15 | "#EEE8D5", 16 | "#002B36", 17 | "#CB4B16", 18 | "#586E75", 19 | "#657B83", 20 | "#839496", 21 | "#6C71C4", 22 | "#93A1A1", 23 | "#FDF6E3" 24 | ] 25 | } -------------------------------------------------------------------------------- /data/schemes/tommorow-night.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tommorow Night", 3 | "comment": "Tommorow Night Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#c5c8c6", 6 | "background-color": "#1d1f21", 7 | "palette": [ 8 | "#000000", 9 | "#cc6666", 10 | "#b5bd68", 11 | "#f0c674", 12 | "#81a2be", 13 | "#b294bb", 14 | "#8abeb7", 15 | "#ffffff", 16 | "#000000", 17 | "#cc6666", 18 | "#b5bd68", 19 | "#f0c674", 20 | "#81a2be", 21 | "#b294bb", 22 | "#8abeb7", 23 | "#ffffff" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/gruvbox-dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gruvbox Dark", 3 | "comment": "Ported for BlackBox Color Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#ebdbb2", 6 | "background-color": "#1d2021", 7 | "palette": [ 8 | "#1d2021", 9 | "#cc241d", 10 | "#98971a", 11 | "#d79921", 12 | "#458588", 13 | "#b16286", 14 | "#689d6a", 15 | "#bdae93", 16 | "#7c6f64", 17 | "#fb4934", 18 | "#b8bb26", 19 | "#fabd2f", 20 | "#83a598", 21 | "#d3869b", 22 | "#8ec07c", 23 | "#ebdbb2" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/dracula-light.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dracula Light", 3 | "comment": "Ported for Terminix Colour Scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#1e1f29", 6 | "background-color": "#ffffff", 7 | "palette": [ 8 | "#000000", 9 | "#ff5555", 10 | "#07f643", 11 | "#d1e30a", 12 | "#bd93f9", 13 | "#ff79c6", 14 | "#04c2eb", 15 | "#bbbbbb", 16 | "#555555", 17 | "#ff5555", 18 | "#07f643", 19 | "#d1e30a", 20 | "#bd93f9", 21 | "#ff79c6", 22 | "#04c2eb", 23 | "#ffffff" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /data/schemes/gruvbox-light.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gruvbox Light", 3 | "comment": "Ported for BlackBox color scheme", 4 | "use-theme-colors": false, 5 | "foreground-color": "#282828", 6 | "background-color": "#f9f5d7", 7 | "palette": [ 8 | "#f9f5d7", 9 | "#cc241d", 10 | "#98971a", 11 | "#d79921", 12 | "#458588", 13 | "#b16286", 14 | "#689d6a", 15 | "#665c54", 16 | "#a89984", 17 | "#9d0006", 18 | "#79740e", 19 | "#b57614", 20 | "#076678", 21 | "#8f3f71", 22 | "#427b58", 23 | "#3c3836" 24 | ] 25 | } 26 | 27 | -------------------------------------------------------------------------------- /data/schemes/base16-twilight-dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base16: Twilight (dark)", 3 | "comment": "@chriskempson's base16 Twilight (dark variant)", 4 | "use-theme-colors": false, 5 | "foreground-color": "#a7a7a7", 6 | "background-color": "#1e1e1e", 7 | "palette": [ 8 | "#1e1e1e", 9 | "#cf6a4c", 10 | "#8f9d6a", 11 | "#f9ee98", 12 | "#7587a6", 13 | "#9b859d", 14 | "#afc4db", 15 | "#a7a7a7", 16 | "#5f5a60", 17 | "#cf6a4c", 18 | "#8f9d6a", 19 | "#f9ee98", 20 | "#7587a6", 21 | "#9b859d", 22 | "#afc4db", 23 | "#ffffff" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.gitlab/issue_templates/bug.md: -------------------------------------------------------------------------------- 1 | ## Bug 2 | 3 | Describe the error in a few words 4 | 5 | ## Steps to Reproduce 6 | 7 | - Do this 8 | - Then that 9 | - Finally this 10 | 11 | ## Expected Outcome 12 | 13 | What did you expect to happen? 14 | 15 | ## Actual Outcome 16 | 17 | What happened instead? 18 | 19 | ## Video or Screenshot 20 | 21 | If possible, please attach a video or screenshot of the error 22 | 23 | ## Information 24 | 25 | 26 | 27 | - Wayland or X11? - 28 | - Are you running Black Box via Flatpak? - 29 | - What version of Black Box? - 30 | - Have you updated your Flatpak dependencies recently? - 31 | - Are you running Wayland + Nvidia? - 32 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | icondir = join_paths(get_option('datadir'), 'icons/hicolor') 2 | pkgicondir = join_paths(get_option('datadir'), 'blackbox/icons/hicolor') 3 | 4 | install_subdir('hicolor/scalable', install_dir: pkgicondir) 5 | 6 | install_data(['hicolor/scalable/apps/com.raggesilver.BlackBox.svg'], 7 | install_dir: join_paths(icondir, 'scalable/apps')) 8 | 9 | install_data(['hicolor/scalable/actions/com.raggesilver.BlackBox-fullscreen-symbolic.svg', 10 | 'hicolor/scalable/actions/com.raggesilver.BlackBox-show-headerbar-symbolic.svg', 11 | 'hicolor/scalable/actions/settings-symbolic.svg', 12 | 'hicolor/scalable/actions/external-link-symbolic.svg', 13 | ], install_dir: join_paths(icondir, 'scalable/actions')) 14 | -------------------------------------------------------------------------------- /src/blackbox.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gtk/header-bar.ui 5 | gtk/help-overlay.ui 6 | gtk/preferences-window.ui 7 | gtk/search-toolbar.ui 8 | gtk/shortcut-dialog.ui 9 | gtk/shortcut-editor.ui 10 | gtk/shortcut-row.ui 11 | gtk/style-switcher.ui 12 | gtk/terminal-menu.ui 13 | gtk/terminal-tab.ui 14 | gtk/tab-menu.ui 15 | style.css 16 | style-dark.css 17 | resources/svg/color-scheme-thumbnail.svg 18 | 19 | icons/scalable/actions/checkmark-small-symbolic.svg 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main.vala: -------------------------------------------------------------------------------- 1 | /* main.vala 2 | * 3 | * Copyright 2020-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 | int main (string[] args) { 20 | return new Terminal.Application ().run (args); 21 | } 22 | -------------------------------------------------------------------------------- /src/vapi/libpcre2-8.vapi: -------------------------------------------------------------------------------- 1 | // https://github.com/elementary/terminal/blob/master/vapi/libpcre2-8.vapi 2 | [CCode (cprefix = "pcre2_", cheader_filename = "pcre2.h")] 3 | namespace PCRE2 { 4 | [Flags] 5 | [CCode (cname = "unsigned int", cprefix = "PCRE2_")] 6 | public enum Flags { 7 | ALLOW_EMPTY_CLASS, 8 | ALT_BSUX, 9 | AUTO_CALLOUT, 10 | CASELESS, 11 | DOLLAR_ENDONLY, 12 | DOTALL, 13 | DUPNAMES, 14 | EXTENDED, 15 | FIRSTLINE, 16 | MATCH_UNSET_BACKREF, 17 | MULTILINE, 18 | NEVER_UCP, 19 | NEVER_UTF, 20 | NO_AUTO_CAPTURE, 21 | NO_AUTO_POSSESS, 22 | NO_DOTSTAR_ANCHOR, 23 | NO_START_OPTIMIZE, 24 | UCP, 25 | UNGREEDY, 26 | UTF, 27 | NEVER_BACKSLASH_C, 28 | ALT_CIRCUMFLEX, 29 | ALT_VERBNAMES, 30 | USE_OFFSET_LIMIT, 31 | EXTENDED_MORE, 32 | LITERAL, 33 | MATCH_INVALID_UTF 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/blackbox-link-system-fonts: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # For some reason `flatpak build ... blackbox` does not give Black Box access to 6 | # the system fonts. Or rather, it does, but GTK (or whatever) cannot find these 7 | # fonts unless I hackily link them. If for some reason this script gets executed 8 | # outside Flatpak, nothing will happen besides running `blackbox`. 9 | 10 | SYM_LINK_TARGET="/run/host/fonts" 11 | SYM_LINK_SOURCE="/run/host/usr/share/fonts" 12 | 13 | # If running in Flatpak, and not inside GNOME Builder, and we have write 14 | # permission to create the symlink, and the target doesn't exist already 15 | 16 | if [ -e /.flatpak-info ] && \ 17 | [ -z $INSIDE_GNOME_BUILDER ] && \ 18 | [ -w $(dirname $SYM_LINK_TARGET) ] && \ 19 | [ ! -e $SYM_LINK_TARGET ] 20 | then 21 | /usr/bin/ln -s $SYM_LINK_SOURCE $SYM_LINK_TARGET 2> /dev/null || echo "Failed to create font symlink" 22 | fi 23 | 24 | blackbox "$@" 25 | -------------------------------------------------------------------------------- /data/schemes/orchis.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Orchis", 3 | "comment": "Tango but using Orchis foreground/background colors", 4 | "foreground-color": "#EFEFEF", 5 | "background-color": "#303030", 6 | "use-theme-colors": false, 7 | "use-highlight-color": false, 8 | "highlight-foreground-color": "#ffffff", 9 | "highlight-background-color": "#a348b1", 10 | "use-cursor-color": false, 11 | "cursor-foreground-color": "#ffffff", 12 | "cursor-background-color": "#efefef", 13 | "use-badge-color": true, 14 | "badge-color": "#ac7ea8", 15 | "palette": [ 16 | "#000000", 17 | "#CC0000", 18 | "#4D9A05", 19 | "#C3A000", 20 | "#3464A3", 21 | "#754F7B", 22 | "#05979A", 23 | "#D3D6CF", 24 | "#545652", 25 | "#EF2828", 26 | "#89E234", 27 | "#FBE84F", 28 | "#729ECF", 29 | "#AC7EA8", 30 | "#34E2E2", 31 | "#EDEDEB" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/gtk/terminal-tab.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 33 | 34 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | data/com.raggesilver.BlackBox.desktop.in 2 | data/com.raggesilver.BlackBox.gschema.xml 3 | src/gtk/shortcut-dialog.ui 4 | src/gtk/shortcut-editor.ui 5 | src/gtk/shortcut-row.ui 6 | src/gtk/search-toolbar.ui 7 | src/gtk/terminal-tab.ui 8 | src/gtk/preferences-window.ui 9 | src/gtk/terminal-menu.ui 10 | src/gtk/help-overlay.ui 11 | src/gtk/tab-menu.ui 12 | src/gtk/header-bar.ui 13 | src/gtk/style-switcher.ui 14 | src/services/ProcessWatcher.vala 15 | src/services/Scheme.vala 16 | src/services/Settings.vala 17 | src/services/Shortcuts.vala 18 | src/services/ThemeProvider.vala 19 | src/utils/Constants.vala 20 | src/utils/Css.vala 21 | src/utils/Dialogs.vala 22 | src/utils/Shortcuts.vala 23 | src/utils/Terminal.vala 24 | src/utils/Widget.vala 25 | src/utils/File.vala 26 | src/widgets/AboutDialog.vala 27 | src/widgets/ColorSchemeThumbnail.vala 28 | src/widgets/HeaderBar.vala 29 | src/widgets/PreferencesWindow.vala 30 | src/widgets/SearchToolbar.vala 31 | src/widgets/ShortcutDialog.vala 32 | src/widgets/ShortcutEditor.vala 33 | src/widgets/StyleSwitcher.vala 34 | src/widgets/TerminalTab.vala 35 | src/widgets/Window.vala 36 | src/widgets/Terminal.vala 37 | src/Application.vala 38 | src/CommandLine.vala 39 | src/main.vala 40 | -------------------------------------------------------------------------------- /src/utils/Shortcuts.vala: -------------------------------------------------------------------------------- 1 | /* Shortcuts.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 | namespace Terminal { 22 | public string get_accel_as_label (string accel) { 23 | Gdk.ModifierType mods; 24 | uint key; 25 | 26 | if (Gtk.accelerator_parse (accel, out key, out mods)) { 27 | var kt = new Gtk.KeyvalTrigger (key, mods); 28 | 29 | return kt.to_label (Gdk.Display.get_default ()); 30 | } 31 | else { 32 | return _("invalid keybinding"); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/Css.vala: -------------------------------------------------------------------------------- 1 | /* Css.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 | namespace Terminal { 22 | public void set_css_class ( 23 | Gtk.Widget widget, 24 | string classname, 25 | bool enabled = true 26 | ) { 27 | if (enabled) { 28 | widget.add_css_class (classname); 29 | } 30 | else { 31 | widget.remove_css_class (classname); 32 | } 33 | } 34 | 35 | public bool is_dark_style_active () { 36 | return Adw.StyleManager.get_default ().dark; 37 | } 38 | 39 | public Gtk.CssProvider? get_css_provider_for_data(string data) { 40 | var provider = new Gtk.CssProvider(); 41 | 42 | provider.load_from_string (data); 43 | 44 | return provider; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/gtk/tab-menu.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | action-disabled 8 | 9 | Move Tab Left 10 | win.move-tab-left 11 | 12 | 13 | 14 | Move Tab Right 15 | win.move-tab-right 16 | 17 | 18 |
19 |
20 | 21 | Rename Tab 22 | win.rename-tab 23 | 24 | 25 | Move Tab to New Window 26 | win.detatch-tab 27 | 28 | 29 |
30 |
31 | 32 | Close Tab 33 | win.close-specific-tab 34 | 35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /com.raggesilver.BlackBox.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "com.raggesilver.BlackBox", 3 | "runtime": "org.gnome.Platform", 4 | "runtime-version": "48", 5 | "sdk": "org.gnome.Sdk", 6 | "command": "blackbox", 7 | "finish-args": [ 8 | "--filesystem=host", 9 | "--filesystem=xdg-run/gvfsd", 10 | "--share=ipc", 11 | "--socket=fallback-x11", 12 | "--socket=wayland", 13 | "--socket=session-bus", 14 | "--device=all", 15 | "--talk-name=org.freedesktop.Flatpak" 16 | ], 17 | "cleanup": [ 18 | "/include", 19 | "/lib/pkgconfig", 20 | "/man", 21 | "/share/doc", 22 | "/share/gtk-doc", 23 | "/share/man", 24 | "/share/pkgconfig", 25 | "/share/vala", 26 | "*.la", 27 | "*.a" 28 | ], 29 | "modules": [ 30 | { 31 | "name": "vte", 32 | "buildsystem": "meson", 33 | "config-opts": ["-Dgtk4=true", "-Dgtk3=false"], 34 | "sources": [ 35 | { 36 | "type": "archive", 37 | "url": "https://gitlab.gnome.org/GNOME/vte/-/archive/0.78.0/vte-0.78.0.tar.gz", 38 | "sha256": "82e19d11780fed4b66400f000829ce5ca113efbbfb7975815f26ed93e4c05f2d" 39 | } 40 | ] 41 | }, 42 | { 43 | "name": "blackbox", 44 | "builddir": true, 45 | "buildsystem": "meson", 46 | "config-opts": ["-Ddevel=true", "-Dblackbox_is_flatpak=true"], 47 | "sources": [ 48 | { 49 | "type": "git", 50 | "url": "https://gitlab.gnome.org/raggesilver/blackbox", 51 | "branch": "main" 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/Widget.vala: -------------------------------------------------------------------------------- 1 | /* Widget.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 | namespace Terminal { 22 | public void widget_set_margin (Gtk.Widget widget, int margin) { 23 | widget.set_margin_top (margin); 24 | widget.set_margin_bottom (margin); 25 | widget.set_margin_start (margin); 26 | widget.set_margin_end (margin); 27 | } 28 | 29 | public void widget_set_css_class (Gtk.Widget widget, 30 | string classname, 31 | bool enabled) 32 | { 33 | var has_class = widget.has_css_class (classname); 34 | 35 | if (has_class && !enabled) { 36 | widget.remove_css_class (classname); 37 | } 38 | else if (!has_class && enabled) { 39 | widget.add_css_class (classname); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/gtk/shortcut-row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 41 | 42 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | subdir('icons') 2 | 3 | install_subdir('schemes', 4 | install_dir: join_paths(get_option('datadir'), 'blackbox')) 5 | 6 | desktop_file = i18n.merge_file( 7 | input: 'com.raggesilver.BlackBox.desktop.in', 8 | output: 'com.raggesilver.BlackBox.desktop', 9 | type: 'desktop', 10 | po_dir: '../po', 11 | install: true, 12 | install_dir: join_paths(get_option('datadir'), 'applications') 13 | ) 14 | 15 | desktop_utils = find_program('desktop-file-validate', required: false) 16 | if desktop_utils.found() 17 | test('Validate desktop file', desktop_utils, 18 | args: [desktop_file] 19 | ) 20 | endif 21 | 22 | appstream_file = i18n.merge_file( 23 | input: 'com.raggesilver.BlackBox.metainfo.xml.in', 24 | output: 'com.raggesilver.BlackBox.metainfo.xml', 25 | po_dir: '../po', 26 | install: true, 27 | install_dir: join_paths(get_option('datadir'), 'metainfo') 28 | ) 29 | 30 | appstream_util = find_program('appstream-util', required: false) 31 | if appstream_util.found() 32 | validate_args = ['validate-relax', appstream_file] 33 | if not get_option('network_tests') 34 | validate_args += '--nonet' 35 | endif 36 | test('Validate appstream file', appstream_util, args: validate_args) 37 | endif 38 | 39 | install_data('com.raggesilver.BlackBox.gschema.xml', 40 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') 41 | ) 42 | 43 | compile_schemas = find_program('glib-compile-schemas', required: false) 44 | if compile_schemas.found() 45 | test('Validate schema file', compile_schemas, 46 | args: ['--strict', '--dry-run', meson.current_source_dir()] 47 | ) 48 | endif 49 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/settings-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /toolbox/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /** 11 | * This is a simple program meant to be launched with flatpak-spawn --host to 12 | * retrieve host information Flatpak'ed apps don't have access to. The original 13 | * idea for this program came from 14 | * https://github.com/gnunn1/tilix/blob/master/experimental/flatpak/tilix-flatpak-toolbox.c 15 | */ 16 | 17 | int die(int code, char *fmt, ...) __attribute__((format(printf, 2, 0))); 18 | 19 | int die(int code, char *fmt, ...) 20 | { 21 | va_list list; 22 | va_start(list, fmt); 23 | vfprintf(stderr, fmt, list); 24 | return code; 25 | } 26 | 27 | int main(int argc, const char *argv[]) 28 | { 29 | if (argc < 2) 30 | { 31 | return die(1, "Not enough arguments.\n"); 32 | } 33 | 34 | if (strcmp(argv[1], "tcgetpgrp") == 0) 35 | { 36 | // This is not needed 37 | if (argc != 3) 38 | { 39 | return die(1, "Missing argument `fd` for tcgetpgrp.\n"); 40 | } 41 | 42 | // This is 3 because we create an array of fds that will be passed to this 43 | // program via a Flatpak DBus call, and the vte pty we need is in the 4th 44 | // slot in that array. 45 | pid_t pid = tcgetpgrp(3); 46 | 47 | printf("%d\n", pid); 48 | 49 | return 0; 50 | } 51 | else if (strcmp(argv[1], "stat") == 0) 52 | { 53 | if (argc < 3) 54 | { 55 | return die(1, "Missing argument 'pid' for getting euid.\n"); 56 | } 57 | 58 | struct stat s; 59 | 60 | if (stat(argv[2], &s) == -1) 61 | { 62 | printf("STAT FAILED FOR PID %s\n", argv[2]); 63 | return -1; 64 | } 65 | 66 | printf("%d\n", s.st_uid); 67 | return 0; 68 | } 69 | else 70 | { 71 | return die(1, "Unknown command '%s'.\n", argv[1]); 72 | } 73 | 74 | return 0; 75 | } 76 | -------------------------------------------------------------------------------- /src/gtk/shortcut-editor.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 49 | 50 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | include: "https://gitlab.gnome.org/GNOME/citemplates/raw/master/flatpak/flatpak_ci_initiative.yml" 2 | 3 | variables: 4 | BUNDLE_NAME: "blackbox.flatpak" 5 | 6 | flatpak: 7 | extends: .flatpak 8 | image: "quay.io/gnome_infrastructure/gnome-runtime-images:gnome-48" 9 | timeout: 90m 10 | variables: 11 | GIT_SUBMODULE_STRATEGY: normal 12 | 13 | # Replace with your manifest path 14 | MANIFEST_PATH: "com.raggesilver.BlackBox.json" 15 | RUNTIME_REPO: "https://flathub.org/repo/flathub.flatpakrepo" 16 | # Replace with your application name, as written in the manifest 17 | FLATPAK_MODULE: "blackbox" 18 | APP_ID: "com.raggesilver.BlackBox" 19 | BUNDLE: ${BUNDLE_NAME} 20 | before_script: 21 | - flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo 22 | 23 | persist_artifacts: 24 | needs: 25 | - job: flatpak 26 | artifacts: true 27 | script: 28 | - PA_JOB_ID=${CI_JOB_ID} 29 | - echo "PA_JOB_ID=$PA_JOB_ID" >> persist_artifacts.env 30 | artifacts: 31 | name: ${BUNDLE_NAME} 32 | paths: 33 | - ${BUNDLE_NAME} 34 | reports: 35 | # To ensure we've access to this file in the next stage 36 | dotenv: persist_artifacts.env 37 | expire_in: never 38 | # Only run this for tag pipelines (releases) 39 | rules: 40 | - if: $CI_COMMIT_TAG 41 | 42 | # https://crypt.codemancers.com/posts/2021-08-31-release-artifacts-using-gitlab-cicd/ 43 | release: 44 | image: registry.gitlab.com/gitlab-org/release-cli:latest 45 | script: 46 | - echo "Releasing ${CI_COMMIT_TAG}" 47 | needs: 48 | - job: persist_artifacts 49 | artifacts: true 50 | release: 51 | name: "Release $CI_COMMIT_TAG" 52 | description: "Created using the release-cli" 53 | tag_name: "$CI_COMMIT_TAG" 54 | assets: 55 | links: 56 | - name: "Flatpak bundle" 57 | url: "https://gitlab.gnome.org/raggesilver/blackbox/-/jobs/${PA_JOB_ID}/artifacts/file/${BUNDLE_NAME}" 58 | # Only run this for tag pipelines (releases) 59 | rules: 60 | - if: $CI_COMMIT_TAG 61 | -------------------------------------------------------------------------------- /src/utils/File.vala: -------------------------------------------------------------------------------- 1 | /* File.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 | public class Terminal.File : Object { 22 | 23 | public GLib.File file { get; protected set; } 24 | 25 | public string path { 26 | owned get { 27 | return this.file.get_path (); 28 | } 29 | } 30 | 31 | public File (string path) { 32 | Object (); 33 | 34 | this.file = GLib.File.new_for_path (path); 35 | } 36 | 37 | public File.new_from_file (GLib.File _file) { 38 | Object (); 39 | 40 | this.file = _file; 41 | } 42 | 43 | public string? read_all (out size_t bytes_read) throws Error { 44 | bytes_read = 0; 45 | 46 | var @is = FileStream.open (this.path, "r"); 47 | @is.seek (0, FileSeek.END); 48 | 49 | size_t size = @is.tell (); 50 | @is.rewind (); 51 | 52 | var buf = new uint8[size]; 53 | var read = @is.read (buf, 1); 54 | 55 | bytes_read = read; 56 | 57 | if (read != size) { 58 | warning ("Invalid read size"); 59 | } 60 | 61 | return read == 0 62 | ? null 63 | : ((string) buf).make_valid ((ssize_t) read); 64 | } 65 | 66 | public void write_plus (string str) throws Error { 67 | var iostream = this.file.replace_readwrite (null, false, FileCreateFlags.NONE); 68 | var os = iostream.output_stream; 69 | os.write_all (str.data, null, null); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/com.raggesilver.BlackBox-show-headerbar-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 39 | 42 | 43 | 45 | 48 | 52 | 53 | 54 | 58 | 66 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/utils/Constants.vala: -------------------------------------------------------------------------------- 1 | namespace Terminal.Constants { 2 | // Copyright (c) 2011-2017 elementary LLC. (https://elementary.io) 3 | // From: https://github.com/elementary/terminal/blob/c3e36fb2ab64c18028ff2b4a6da5bfb2171c1c04/src/Widgets/TerminalWidget.vala 4 | const string USERCHARS = "-[:alnum:]"; 5 | const string USERCHARS_CLASS = "[" + USERCHARS + "]"; 6 | const string PASSCHARS_CLASS = "[-[:alnum:]\\Q,?;.:/!%$^*&~\"#'\\E]"; 7 | const string HOSTCHARS_CLASS = "[-[:alnum:]]"; 8 | const string HOST = HOSTCHARS_CLASS + "+(\\." + HOSTCHARS_CLASS + "+)*"; 9 | const string PORT = "(?:\\:[[:digit:]]{1,5})?"; 10 | const string PATHCHARS_CLASS = "[-[:alnum:]\\Q_$.+!*,;:@&=?/~#%\\E]"; 11 | const string PATHTERM_CLASS = "[^\\Q]'.}>) \t\r\n,\"\\E]"; 12 | const string SCHEME = 13 | "(?:news:|telnet:|nntp:|file:|https?:|ftps?:|sftp:|webcal:" + 14 | "|irc:|sftp:|ldaps?:|nfs:|smb:|rsync:|ssh:|rlogin:|telnet:|git:" + 15 | "|git\\+ssh:|bzr:|bzr\\+ssh:|svn:|svn\\+ssh:|hg:|mailto:|magnet:)"; 16 | 17 | const string USERPASS = USERCHARS_CLASS + "+(?:" + PASSCHARS_CLASS + "+)?"; 18 | const string URLPATH = "(?:(/" + PATHCHARS_CLASS + 19 | "+(?:[(]" + PATHCHARS_CLASS + 20 | "*[)])*" + PATHCHARS_CLASS + 21 | "*)*" + PATHTERM_CLASS + 22 | ")?"; 23 | 24 | const string[] URL_REGEX_STRINGS = { 25 | SCHEME + "//(?:" + USERPASS + "\\@)?" + HOST + PORT + URLPATH, 26 | "(?:www|ftp)" + HOSTCHARS_CLASS + "*\\." + HOST + PORT + URLPATH, 27 | "(?:callto:|h323:|sip:)" + USERCHARS_CLASS + "[" + USERCHARS + ".]*(?:" 28 | + PORT + "/[a-z0-9]+)?\\@" + HOST, 29 | "(?:mailto:)?" + USERCHARS_CLASS + "[" + USERCHARS + ".]*\\@" 30 | + HOSTCHARS_CLASS + "+\\." + HOST, 31 | "(?:news:|man:|info:)[[:alnum:]\\Q^_{|}~!\"#$%&'()*+,./;:=?`\\E]+" 32 | }; 33 | 34 | const string MENU_BUTTON_ALTERNATIVE = _("You can still access the menu by right-clicking any terminal."); 35 | 36 | const string COPYING_NOT_IMPLEMENTED_WARNING_FMT = _("%s uses an early Gtk 4 port of VTE as a terminal widget. While a lot of progress has been made on this port, copying has yet to be implemented. This means there's currently no way to copy text in %s."); 37 | 38 | const string INFINITE_SCROLLBACK_WARNING = _("Warning: unlimited scrollback saves content to disk, which may cause your system to run out of storage."); 39 | 40 | public string get_user_schemes_dir () { 41 | return Path.build_path( 42 | Path.DIR_SEPARATOR_S, Environment.get_user_data_dir (), "blackbox", "schemes" 43 | ); 44 | } 45 | 46 | public string get_user_keybindings_path () { 47 | return Path.build_path( 48 | Path.DIR_SEPARATOR_S, Environment.get_user_data_dir (), "blackbox", "user-keymap.json" 49 | ); 50 | } 51 | 52 | public string get_app_schemes_dir () { 53 | return Path.build_path ( 54 | Path.DIR_SEPARATOR_S, DATADIR, "blackbox", "schemes" 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/services/Scheme.vala: -------------------------------------------------------------------------------- 1 | /* Scheme.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 | public class Terminal.Scheme : Object, Json.Serializable { 22 | public string name { get; set; } 23 | public string? comment { get; set; } 24 | public Gdk.RGBA? foreground_color { get; set; } 25 | public Gdk.RGBA? background_color { get; set; } 26 | public Array? palette { get; set; } 27 | 28 | public bool is_dark { 29 | get { 30 | return get_color_brightness (this.foreground_color) > 0.5f; 31 | } 32 | } 33 | 34 | public override bool deserialize_property ( 35 | string name, 36 | out Value @value, 37 | ParamSpec spec, 38 | Json.Node node 39 | ) { 40 | // Supress "possibly unitialized value" 41 | @value = Value(spec.value_type); 42 | 43 | switch (name) { 44 | case "foreground-color": 45 | case "background-color": { 46 | var data = node.get_string (); 47 | var color = rgba_from_string (data); 48 | 49 | if (color == null) return false; 50 | 51 | @value = color; 52 | return true; 53 | } 54 | 55 | case "palette": { 56 | var arr = node.get_array (); 57 | 58 | var res = new Array (); 59 | 60 | for (uint i = 0; i < arr.get_length (); i++) { 61 | var data = arr.get_string_element (i); 62 | var color = rgba_from_string (data); 63 | 64 | if (color == null) return false; 65 | 66 | res.append_val (color); 67 | } 68 | 69 | @value = res; 70 | return true; 71 | } 72 | } 73 | 74 | return default_deserialize_property (name, out @value, spec, node); 75 | } 76 | 77 | public static Scheme? from_file (File file) throws Error { 78 | size_t length; 79 | string data = file.read_all (out length); 80 | 81 | var scheme = (Scheme) Json.gobject_from_data ( 82 | typeof (Scheme), 83 | data 84 | ); 85 | 86 | if ( 87 | scheme.name == null || 88 | scheme.foreground_color == null || 89 | scheme.background_color == null || 90 | scheme.palette?.length != 16 91 | ) { 92 | return null; 93 | } 94 | 95 | return scheme; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/widgets/StyleSwitcher.vala: -------------------------------------------------------------------------------- 1 | /* StyleSwitcher.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 | 22 | [GtkTemplate (ui = "/com/raggesilver/BlackBox/gtk/style-switcher.ui")] 23 | public class Terminal.StyleSwitcher : Gtk.Widget { 24 | 25 | [GtkChild] unowned Gtk.CheckButton system_selector; 26 | [GtkChild] unowned Gtk.CheckButton light_selector; 27 | [GtkChild] unowned Gtk.CheckButton dark_selector; 28 | 29 | public uint style { get; set; } 30 | public bool show_system { get; set; default = true; } 31 | 32 | static construct { 33 | set_layout_manager_type (typeof (Gtk.BinLayout)); 34 | set_css_name ("themeswitcher"); 35 | } 36 | 37 | construct { 38 | this.notify ["style"].connect (this.on_style_changed); 39 | 40 | var s = Settings.get_default (); 41 | s.bind_property ("style-preference", 42 | this, 43 | "style", 44 | GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL, 45 | null, 46 | null); 47 | } 48 | 49 | private void on_style_changed () { 50 | this.freeze_notify (); 51 | if (this.style == ApplicationStyle.SYSTEM) { 52 | this.system_selector.active = true; 53 | this.light_selector.active = false; 54 | this.dark_selector.active = false; 55 | } 56 | else if (this.style == ApplicationStyle.LIGHT) { 57 | this.system_selector.active = false; 58 | this.light_selector.active = true; 59 | this.dark_selector.active = false; 60 | } 61 | else { 62 | this.system_selector.active = false; 63 | this.light_selector.active = false; 64 | this.dark_selector.active = true; 65 | } 66 | this.thaw_notify (); 67 | } 68 | 69 | [GtkCallback] 70 | private void theme_check_active_changed () { 71 | if (this.system_selector.active) { 72 | if (this.style != ApplicationStyle.SYSTEM) { 73 | this.style = (uint) ApplicationStyle.SYSTEM; 74 | } 75 | } 76 | else if (this.light_selector.active) { 77 | if (this.style != ApplicationStyle.LIGHT) { 78 | this.style = (uint) ApplicationStyle.LIGHT; 79 | } 80 | } 81 | else { 82 | if (this.style != ApplicationStyle.DARK) { 83 | this.style = (uint) ApplicationStyle.DARK; 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/apps/com.raggesilver.BlackBox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | /* libadwaita does 0.85, but 0.8 looks better */ 2 | @define-color fixed-borders mix(@window_fg_color, @window_bg_color, 0.8); 3 | 4 | @define-color root_context_color shade(@red_1, 1.38); 5 | @define-color ssh_context_color shade(@purple_1, 1.28); 6 | 7 | /* #blackbox-main-window.with-borders:not(.fullscreen):backdrop { 8 | border-color: alpha(@borders, 0.5); 9 | } */ 10 | 11 | #blackbox-main-window { 12 | background: transparent; 13 | } 14 | 15 | #blackbox-main-window scrollbar { 16 | background: @window_bg_color; 17 | } 18 | 19 | window:not(.about) headerbar, 20 | .custom-headerbar { 21 | background-color: @window_bg_color; 22 | } 23 | 24 | #blackbox-main-window.context-root .custom-headerbar { 25 | background-color: @root_context_color; 26 | } 27 | 28 | #blackbox-main-window.context-ssh .custom-headerbar { 29 | background-color: @ssh_context_color; 30 | } 31 | 32 | .custom-headerbar windowhandle > box { 33 | padding-top: 0; 34 | padding-bottom: 0; 35 | } 36 | 37 | .custom-headerbar windowcontrols:not(.empty).start { 38 | margin-right: 6px; 39 | } 40 | 41 | .custom-headerbar windowcontrols.start.empty ~ tabbar { 42 | margin-left: -6px; 43 | } 44 | 45 | .custom-headerbar windowcontrols:not(.empty).end { 46 | margin-left: 6px; 47 | } 48 | 49 | /** 50 | * Add space on the left when "Drag Area" is enabled. 51 | */ 52 | .custom-headerbar.with-dragarea:not(.single-tab-mode) 53 | windowcontrols.start.empty 54 | ~ tabbar { 55 | margin-left: 34px; 56 | } 57 | 58 | .toolbar { 59 | background-color: @headerbar_bg_color; 60 | } 61 | 62 | .toolbar > * { 63 | padding: 2px 8px; 64 | } 65 | 66 | .thumbnail picture { 67 | border: 2px solid transparent; 68 | } 69 | 70 | .thumbnail picture.selected { 71 | border-color: @accent_bg_color; 72 | } 73 | 74 | .thumbnail image { 75 | background-color: @accent_bg_color; 76 | border-radius: 9999px; 77 | padding: 2px; 78 | color: white; 79 | margin: 8px; 80 | } 81 | 82 | .shortcut-row { 83 | padding: 12px; 84 | } 85 | 86 | themeswitcher { 87 | padding: 6px; 88 | } 89 | 90 | themeswitcher .check { 91 | background: @accent_bg_color; 92 | color: @accent_fg_color; 93 | padding: 2px; 94 | border-radius: 17px; 95 | margin: 3px; 96 | } 97 | 98 | /* Adapted from https://gitlab.gnome.org/GNOME/gnome-text-editor/-/blob/bf8c0c249f06a0be69e65aed3b786ba02a9f999e/src/TextEditor.css#L51 */ 99 | 100 | themeswitcher checkbutton { 101 | outline-offset: 1px; 102 | transition: none; 103 | } 104 | 105 | themeswitcher checkbutton radio { 106 | -gtk-icon-source: none; 107 | background: none; 108 | padding: 12px; 109 | min-height: 24px; 110 | min-width: 24px; 111 | border: none; 112 | outline-color: currentColor; 113 | transition: none; 114 | box-shadow: inset 0 0 0 1px @borders; 115 | } 116 | 117 | themeswitcher checkbutton radio:checked { 118 | box-shadow: inset 0 0 0 2px @accent_bg_color; 119 | } 120 | 121 | themeswitcher checkbutton.system radio { 122 | background: linear-gradient(-45deg, #1e1e1e 49.99%, white 50.01%); 123 | } 124 | 125 | themeswitcher checkbutton.light radio { 126 | color: alpha(black, 0.8); 127 | background-color: white; 128 | } 129 | 130 | themeswitcher checkbutton.dark radio { 131 | color: white; 132 | background-color: #1e1e1e; 133 | } 134 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | blackbox_sources = files( 2 | 'Application.vala', 3 | 'CommandLine.vala', 4 | 'main.vala', 5 | ) 6 | 7 | subdir('widgets') 8 | subdir('services') 9 | subdir('utils') 10 | 11 | conf_data = configuration_data() 12 | conf_data.set_quoted('PROJECT_NAME', meson.project_name()) 13 | conf_data.set_quoted('GETTEXT_PACKAGE', meson.project_name()) 14 | conf_data.set_quoted('VERSION', meson.project_version()) 15 | conf_data.set_quoted('PREFIX', get_option('prefix')) 16 | conf_data.set_quoted('DATADIR', join_paths (get_option('prefix'), get_option('datadir'))) 17 | conf_data.set_quoted('LOCALEDIR', join_paths (get_option('prefix'), get_option('localedir'))) 18 | conf_data.set('DEVEL', get_option('devel').to_string().to_lower()) 19 | 20 | config_header = configure_file( 21 | input: 'config.vala.in', 22 | output: 'config.vala', 23 | configuration: conf_data 24 | ) 25 | 26 | # blackbox_deps = [ 27 | # dependency('gio-2.0', version: '>= 2.73'), 28 | # dependency('gtk4', version: '>= 4.8'), 29 | # dependency('libadwaita-1', version: '>= 1.2'), 30 | # dependency('marble', version: '>= 42'), 31 | # dependency('vte-2.91-gtk4', version: '>= 0.71'), 32 | # dependency('json-glib-1.0', version: '>= 1.6'), 33 | # dependency('libpcre2-8', version: '>= 10.40'), 34 | # dependency('libxml-2.0', version: '>= 2.10'), 35 | # dependency('librsvg-2.0', version: '>= 2.55'), 36 | # dependency('gee-0.8', version: '>=0.20'), 37 | # dependency('graphene-gobject-1.0', version: '>= 1.11'), 38 | # ] 39 | 40 | blackbox_deps = [ 41 | dependency('gio-2.0', version: '>= 2.50'), 42 | dependency('gtk4', version: '>= 4.12.0'), 43 | dependency('libadwaita-1', version: '>= 1.4'), 44 | dependency('vte-2.91-gtk4', version: '>= 0.69.0'), 45 | dependency('json-glib-1.0', version: '>= 1.4.4'), 46 | dependency('libpcre2-8'), 47 | dependency('libxml-2.0', version: '>= 2.9.12'), 48 | dependency('librsvg-2.0', version: '>= 2.54.0'), 49 | dependency('gee-0.8', version: '>=0.20'), 50 | dependency('graphene-gobject-1.0'), 51 | meson.get_compiler('vala').find_library('posix'), 52 | ] 53 | 54 | # https://github.com/elementary/terminal/blob/d9620eb12331a28c658f97ac9a1bdb809aa90089/meson.build 55 | vapi_dir = meson.current_source_dir() / 'vapi' 56 | add_project_arguments('--vapidir=' + vapi_dir, language: 'vala') 57 | add_project_arguments('-DPCRE2_CODE_UNIT_WIDTH=0', language: 'c') 58 | 59 | # This is required until VTE 0.70 is out 60 | add_project_arguments('--disable-since-check', language: 'vala') 61 | 62 | gnome = import('gnome') 63 | 64 | blackbox_sources += gnome.compile_resources('blackbox-resources', 65 | 'blackbox.gresource.xml', 66 | c_name: 'blackbox' 67 | ) 68 | 69 | if get_option('blackbox_debug_memory') 70 | add_project_arguments('-D', 'BLACKBOX_DEBUG_MEMORY', language: 'vala') 71 | endif 72 | 73 | if get_option('blackbox_is_flatpak') 74 | add_project_arguments('-D', 'BLACKBOX_IS_FLATPAK', language: 'vala') 75 | endif 76 | 77 | executable('blackbox', blackbox_sources, config_header, 78 | vala_args: '--target-glib=2.50', dependencies: blackbox_deps, 79 | install: true, 80 | ) 81 | 82 | if get_option('devel') 83 | custom_target ('flatpak-exe', 84 | input: 'blackbox-link-system-fonts', 85 | output: ['blackbox-link-system-fonts'], 86 | command: ['cp', '@INPUT@', '@OUTPUT@'], 87 | install: true, 88 | install_dir: 'bin') 89 | endif 90 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/com.raggesilver.BlackBox-fullscreen-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 39 | 42 | 43 | 45 | 48 | 52 | 53 | 54 | 58 | 61 | 66 | 70 | 75 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to Black Box 2 | 3 | Thank you for considering contributing to Black Box! 4 | 5 | This guide aims to make it easier for us to communicate and further develop 6 | Black Box. 7 | 8 | You can contribute in many ways: reporting bugs, providing feedback, translating 9 | the app, or writing code. 10 | 11 | ## Plan ahead 12 | 13 | If you plan to develop a new feature for Black Box, 14 | [open a feature request first.](https://gitlab.gnome.org/raggesilver/blackbox/-/issues/new?issuable_template=feature_request&issue[issue_type]=issue) 15 | This ensures we can discuss the idea and the implementation beforehand. Remember 16 | that not all feature requests will be accepted, as some might diverge from Black 17 | Box's philosophy. 18 | 19 | ## Bugs and Feature Requests 20 | 21 | Black Box has templates for bugs and feature requests. Please use them! 22 | 23 | - [Feature request](https://gitlab.gnome.org/raggesilver/blackbox/-/issues/new?issuable_template=feature_request&issue[issue_type]=issue) 24 | - [Bug report](https://gitlab.gnome.org/raggesilver/blackbox/-/issues/new?issuable_template=bug&issue[issue_type]=issue) 25 | - [Security issue](https://gitlab.gnome.org/raggesilver/blackbox/-/issues/new?issuable_template=bug&issue[issue_type]=issue&issue[confidential]=true) - make 26 | sure "confidential" is checked 27 | 28 | ## Translating 29 | 30 | Black Box is accepting translations through Weblate! If you'd like to 31 | contribute with translations, visit the 32 | [Weblate project](https://hosted.weblate.org/projects/blackbox/). 33 | 34 | 35 | Translation status 36 | 37 | 38 | ## Writing Code 39 | 40 | Before writing code, make sure you have followed [these instructions](#plan-ahead). 41 | 42 | ### Setup 43 | 44 | I recommend using [Visual Studio Code](https://code.visualstudio.com/) with the 45 | following extensions: 46 | 47 | - [Vala](https://marketplace.visualstudio.com/items?itemName=prince781.vala) 48 | - [Editorconfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) 49 | - [Flatpak](https://marketplace.visualstudio.com/items?itemName=bilelmoussaoui.flatpak-vscode) 50 | 51 | ### Code Style 52 | 53 | Black Box follows the Prettier code style as much as possible. With one 54 | exception: brace style. 55 | 56 | ```vala 57 | public class Terminal.YourClass : YourParent { 58 | construct { 59 | // ... 60 | } 61 | 62 | public YourClass () { 63 | Object (); 64 | 65 | this.notify ["my-change"].connect (this.my_callback); 66 | } 67 | 68 | private void my_callback ( 69 | int x, 70 | int y, 71 | int n_times 72 | ) { 73 | if (x < 0) { 74 | // ... 75 | } 76 | else if (x > 9) { 77 | // ... 78 | } 79 | else { 80 | // ... 81 | try { 82 | // ... 83 | } 84 | catch (Error e) { 85 | // ... 86 | } 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | ### Reactivity 93 | 94 | Avoid using `notify` to update the UI as much as possible. Using 95 | [GLib.Settings.bind](https://valadoc.org/gio-2.0/GLib.Settings.bind.html), 96 | [GLib.Settings.bind_with_mapping](https://valadoc.org/gio-2.0/GLib.Settings.bind_with_mapping.html), 97 | or [GLib.Object.bind_property](https://valadoc.org/gobject-2.0/GLib.Object.bind_property.html) 98 | is preferred. 99 | 100 | ### Warnings 101 | 102 | Do your best not to introduce new compilation or runtime warnings to the app. 103 | Warnings make it harder to debug errors and give users the impression that the 104 | app is not working as it should. 105 | -------------------------------------------------------------------------------- /src/widgets/HeaderBar.vala: -------------------------------------------------------------------------------- 1 | /* HeaderBar.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/header-bar.ui")] 22 | public class Terminal.HeaderBar : Adw.Bin { 23 | 24 | [GtkChild] public unowned Adw.TabBar tab_bar; 25 | 26 | public Window window { get; set; } 27 | public Settings settings { get; construct set; } 28 | public bool floating_mode { get; set; default = false; } 29 | 30 | public bool single_tab_mode { 31 | get { 32 | var settings = Settings.get_default (); 33 | return ( 34 | (this.window == null || this.window.tab_view.n_pages <= 1) && 35 | settings.fill_tabs 36 | ); 37 | } 38 | } 39 | 40 | static construct { 41 | set_css_name ("headerbar"); 42 | typeof (StyleSwitcher).class_ref (); 43 | } 44 | 45 | construct { 46 | this.settings = Settings.get_default (); 47 | } 48 | 49 | public HeaderBar (Window window) { 50 | Object (window: window); 51 | 52 | this.connect_signals (); 53 | } 54 | 55 | private void connect_signals () { 56 | var settings = Settings.get_default (); 57 | 58 | this.window.tab_view.notify ["n-pages"] 59 | .connect (this.notify_single_tab_mode); 60 | 61 | settings.notify ["fill-tabs"].connect (this.notify_single_tab_mode); 62 | 63 | settings.notify ["headerbar-drag-area"].connect ( 64 | this.on_drag_area_changed 65 | ); 66 | this.on_drag_area_changed (); 67 | 68 | this.notify ["single-tab-mode"].connect (this.on_single_tab_mode_changed); 69 | this.on_single_tab_mode_changed (); 70 | 71 | var mcc = new Gtk.GestureClick () { 72 | button = Gdk.BUTTON_MIDDLE, 73 | }; 74 | mcc.pressed.connect (() => { 75 | this.window.new_tab (null, null); 76 | }); 77 | this.add_controller (mcc); 78 | } 79 | 80 | [GtkCallback] 81 | private void unfullscreen () { 82 | this.window.unfullscreen (); 83 | } 84 | 85 | [GtkCallback] 86 | private bool show_window_controls ( 87 | bool fullscreened, 88 | bool _is_floating, 89 | bool _is_single_tab_mode, 90 | bool is_header_bar_controls 91 | ) { 92 | return ( 93 | (this.window == null || !fullscreened) && 94 | (!_is_floating) && 95 | (!is_header_bar_controls || _is_single_tab_mode) 96 | ); 97 | } 98 | 99 | [GtkCallback] 100 | private string get_visible_stack_name (bool is_single_tab_mode) { 101 | return is_single_tab_mode ? "single-tab-page" : "multi-tab-page"; 102 | } 103 | 104 | private void notify_single_tab_mode () { 105 | this.notify_property ("single-tab-mode"); 106 | } 107 | 108 | private void on_drag_area_changed () { 109 | var drag_area = Settings.get_default ().headerbar_drag_area; 110 | 111 | set_css_class (this, "with-dragarea", drag_area); 112 | } 113 | 114 | private void on_single_tab_mode_changed () { 115 | bool single_tab_enabled = this.single_tab_mode; 116 | 117 | set_css_class (this, "single-tab-mode", single_tab_enabled); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/gtk/shortcut-dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 94 | 95 | -------------------------------------------------------------------------------- /src/gtk/terminal-menu.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 25 | 26 |
27 | 28 | New Tab 29 | win.new_tab 30 | 31 | 32 | New Window 33 | app.new-window 34 | 35 |
36 | 37 | 52 | 53 |
54 | 55 | Copy 56 | win.copy 57 | 58 | 59 | Copy Link 60 | win.copy-link 61 | action-disabled 62 | 63 | 64 | Paste 65 | win.paste 66 | 67 |
68 | 69 |
70 | 71 | Preferences 72 | win.edit_preferences 73 | 74 |
75 | 76 |
77 | 78 | Keyboard Shortcuts 79 | win.show-help-overlay 80 | 81 | 82 | About Black Box 83 | app.about 84 | 85 |
86 |
87 | 88 | 89 | terminal-menu 90 | 110 | 111 |
112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Black Box

3 |

An elegant and customizable terminal for GNOME

4 |

5 | Features • 6 | Install • 7 | Gallery • 8 | Changelog 9 |
10 | Wiki • 11 | License • 12 | Contributing 13 |

14 |

15 |

16 |
17 | 18 |
19 | Preview
20 | 21 | Black Box 0.14.0 (theme "Japanesque", fetch bunnyfetch) 22 | 23 |

24 |
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 | Download on Flathub 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 | Translation status 79 | 80 | 81 | ## Gallery 82 | 83 | > Some of these screenshot are from older versions of Black Box. 84 | 85 |
86 | Black Box with 'Show Header bar' off
87 | 88 | Black Box with "show header bar" off. 89 | 90 |

91 | Black Box with 'Show Header bar' off
92 | 93 | Black Box with transparent background* and sixel support. *blur is controled 94 | by your compositor. 95 | 96 |

97 |
98 | 99 | ## Credits 100 | 101 | - Most of Black Box's themes come (straight out copied) from [Tilix](https://github.com/gnunn1/tilix) 102 | - Most non-Tilix-default themes come (straight out copied) from [Tilix-Themes](https://github.com/storm119/Tilix-Themes) 103 | - Thank you, @linuxllama, for QA testing and creating Black Box's app icon 104 | - Thank you, @predvodnik, for coming up with the name "Black Box" 105 | - Source code that derives from other projects is properly attributed in the code itself 106 | -------------------------------------------------------------------------------- /src/CommandLine.vala: -------------------------------------------------------------------------------- 1 | /* CommandLine.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 | public struct Terminal.CommandLineOptions { 22 | string? command; 23 | string? current_working_dir; 24 | bool version; 25 | bool help; 26 | } 27 | 28 | // Usage: 29 | // blackbox [OPTION…] [-- COMMAND ...] 30 | // 31 | // Options: 32 | // -v, --version Show app version 33 | // -w, --working-directory Set current working directory 34 | // -c, --command Execute command in a terminal 35 | // -h, --help Show help 36 | 37 | public class Terminal.CommandLine { 38 | public static bool parse_command_line (GLib.ApplicationCommandLine cmd, 39 | out CommandLineOptions options) 40 | { 41 | options = {}; 42 | 43 | OptionEntry[] option_entries = { 44 | OptionEntry () { 45 | long_name = "version", 46 | short_name = 'v', 47 | description = _("Show app version"), 48 | flags = OptionFlags.NONE, 49 | arg = OptionArg.NONE, 50 | arg_data = &options.version, 51 | arg_description = null, 52 | }, 53 | OptionEntry () { 54 | long_name = "working-directory", 55 | short_name = 'w', 56 | description = _("Set current working directory"), 57 | flags = OptionFlags.NONE, 58 | arg = OptionArg.FILENAME, 59 | arg_data = &options.current_working_dir, 60 | arg_description = null, 61 | }, 62 | OptionEntry () { 63 | long_name = "command", 64 | short_name = 'c', 65 | description = _("Execute command in a terminal"), 66 | flags = OptionFlags.NONE, 67 | arg = OptionArg.STRING, 68 | arg_data = &options.command, 69 | arg_description = null, 70 | }, 71 | OptionEntry () { 72 | long_name = "help", 73 | short_name = 'h', 74 | description = _("Show help"), 75 | flags = OptionFlags.NONE, 76 | arg = OptionArg.NONE, 77 | arg_data = &options.help, 78 | arg_description = null, 79 | }, 80 | }; 81 | 82 | var ctx = new OptionContext ("[-- COMMAND ...]"); 83 | // If this is set to true and the user launches blackbox with --help, the 84 | // entire GTK application will close (with exit(0)), even if there are other 85 | // windows open 86 | ctx.set_help_enabled (false); 87 | ctx.add_main_entries (option_entries, null); 88 | 89 | var original_argv = cmd.get_arguments (); 90 | string[] real_argv = {}; 91 | string[] commandv = {}; 92 | bool dd = false; 93 | 94 | // Check if "--" is present. If so, everything after it will be appended to 95 | // `commandv` and fed as a single command to the terminal. 96 | foreach (unowned string s in original_argv) { 97 | if (dd) { 98 | commandv += s; 99 | } 100 | else if (s == "--") { 101 | dd = true; 102 | } 103 | else { 104 | real_argv += s; 105 | } 106 | } 107 | 108 | try { 109 | ctx.parse_strv (ref real_argv); 110 | 111 | if (options.help) { 112 | cmd.print_literal (ctx.get_help (true, null)); 113 | } 114 | // If "--" was present and "-c" wasn't set 115 | if (dd && options.command == null) { 116 | options.command = string.joinv (" ", commandv); 117 | } 118 | } 119 | catch (Error e) { 120 | cmd.printerr ("%s\n", e.message); 121 | cmd.printerr (_("Run %s --help to get help\n"), original_argv[0]); 122 | return false; 123 | } 124 | 125 | return true; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/gtk/style-switcher.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 100 | 101 | -------------------------------------------------------------------------------- /src/utils/Dialogs.vala: -------------------------------------------------------------------------------- 1 | /* Dialogs.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 | namespace Terminal { 22 | public enum ConfirmClosingContext { 23 | TAB, 24 | WINDOW, 25 | } 26 | 27 | public enum ConfirmActionType { 28 | NO_YES, 29 | CANCEL_OK, 30 | KEEP_REPLACE, 31 | CANCEL_CLOSE; 32 | 33 | public string[] get_labels () { 34 | switch (this) { 35 | case NO_YES: return { _("No"), _("Yes") }; 36 | case CANCEL_OK: return { _("Cancel"), _("OK") }; 37 | case KEEP_REPLACE: return { _("Keep"), _("Replace") }; 38 | case CANCEL_CLOSE: return { _("Cancel"), _("Close") }; 39 | } 40 | error ("Invalid ConfirmActionType %d", this); 41 | } 42 | 43 | public string[] get_values () { 44 | switch (this) { 45 | case NO_YES: return { "no", "yes" }; 46 | case CANCEL_OK: return { "cancel", "ok" }; 47 | case KEEP_REPLACE: return { "keep", "replace" }; 48 | case CANCEL_CLOSE: return { "cancel", "close" }; 49 | } 50 | error ("Invalid ConfirmActionType %d", this); 51 | } 52 | } 53 | 54 | public async bool confirm_action ( 55 | string title, 56 | string body, 57 | ConfirmActionType confirm_type = ConfirmActionType.CANCEL_OK, 58 | Adw.ResponseAppearance appearance1 = Adw.ResponseAppearance.DEFAULT, 59 | Adw.ResponseAppearance appearance2 = Adw.ResponseAppearance.DEFAULT, 60 | Gtk.Widget? extra_child = null 61 | ) { 62 | bool response = false; 63 | SourceFunc callback = confirm_action.callback; 64 | 65 | string[] labels = confirm_type.get_labels (); 66 | string[] values = confirm_type.get_values (); 67 | 68 | var d = new Adw.AlertDialog ( 69 | title, 70 | body 71 | ) { 72 | default_response = values [1], 73 | close_response = values [0], 74 | extra_child = extra_child 75 | }; 76 | 77 | d.add_response (values [0], labels [0]); 78 | d.add_response (values [1], labels [1]); 79 | 80 | d.set_response_appearance (values [0], appearance1); 81 | d.set_response_appearance (values [1], appearance2); 82 | 83 | d.response.connect ((_response) => { 84 | response = _response == values [1]; 85 | callback (); 86 | }); 87 | 88 | d.present ((GLib.Application.get_default () as Gtk.Application)?.get_active_window ()); 89 | 90 | yield; 91 | 92 | return response; 93 | } 94 | 95 | public async bool confirm_closing ( 96 | string?[]? commands, 97 | ConfirmClosingContext context = ConfirmClosingContext.TAB 98 | ) { 99 | Gtk.Widget? child = null; 100 | 101 | if (commands != null && commands.length > 0) { 102 | var list = new Gtk.ListBox () { 103 | css_classes = { "boxed-list" }, 104 | selection_mode = Gtk.SelectionMode.NONE, 105 | }; 106 | 107 | foreach (unowned string command in commands) { 108 | var row = new Adw.ActionRow () { 109 | title = command ?? "", 110 | css_classes = { "monospace" }, 111 | can_focus = false, 112 | }; 113 | list.append (row); 114 | } 115 | child = list; 116 | } 117 | 118 | string title = context == ConfirmClosingContext.TAB 119 | ? _("Close Tab?") 120 | : _("Close Window?"); 121 | 122 | string body = context == ConfirmClosingContext.TAB 123 | ? _("Some commands are still running. Closing this tab will kill them and may lead to unexpected outcomes.") 124 | : _("Some commands are still running. Closing this window will kill them and may lead to unexpected outcomes."); 125 | 126 | var response = yield confirm_action ( 127 | title, 128 | body, 129 | ConfirmActionType.CANCEL_CLOSE, 130 | Adw.ResponseAppearance.DEFAULT, 131 | Adw.ResponseAppearance.DESTRUCTIVE, 132 | child 133 | ); 134 | 135 | return response; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/gtk/search-toolbar.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 79 | 80 | 81 | 82 | 83 | vertical 84 | 87 | 88 | 89 | Search wraps around 90 | 91 | 92 | 93 | 94 | Match is case sensitive 95 | 96 | 97 | 98 | 99 | Match entire words only 100 | 101 | 102 | 103 | 104 | Match using regular expression 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/Application.vala: -------------------------------------------------------------------------------- 1 | /* Application.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 | public class Terminal.Application : Adw.Application { 22 | private ActionEntry[] ACTIONS = { 23 | { "focus-next-tab", on_focus_next_tab }, 24 | { "focus-previous-tab", on_focus_previous_tab }, 25 | { "new-window", on_new_window }, 26 | { "about", on_about }, 27 | // { "quit", on_app_quit }, 28 | }; 29 | 30 | public Application () { 31 | Object ( 32 | application_id: "com.raggesilver.BlackBox", 33 | flags: ApplicationFlags.HANDLES_COMMAND_LINE 34 | ); 35 | 36 | Intl.setlocale (LocaleCategory.ALL, ""); 37 | Intl.textdomain (GETTEXT_PACKAGE); 38 | Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); 39 | Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); 40 | 41 | this.add_action_entries (ACTIONS, this); 42 | 43 | var focus_tab = new SimpleAction("focus-tab", new VariantType("(uu)")); 44 | focus_tab.activate.connect ((action, variant) => { 45 | var window_id = variant.get_child_value(0).get_uint32(); 46 | var tab_id = variant.get_child_value(1).get_uint32(); 47 | this.on_focus_tab(window_id, tab_id); 48 | }); 49 | this.add_action(focus_tab); 50 | 51 | var keymap = Keymap.get_default (); 52 | keymap.apply (this); 53 | } 54 | 55 | public override void activate () { 56 | // Avoid opening a new window if one is already open 57 | foreach (var window in this.get_windows ()) { 58 | if (window is Window) { 59 | // This seems the right way to go about "reclaiming" focus for a window. 60 | // However, it doesn't work on Xorg nor on Wayland - it does nothing. 61 | window.present (); 62 | return; 63 | } 64 | } 65 | 66 | new Window (this).show (); 67 | } 68 | 69 | public override int command_line (GLib.ApplicationCommandLine cmd) { 70 | CommandLineOptions options; 71 | 72 | this.hold (); 73 | 74 | if (!CommandLine.parse_command_line (cmd, out options)) { 75 | this.release (); 76 | return -1; 77 | } 78 | else if (options.help) { 79 | // For logistical reasons help is handled in `parse_command_line`. 80 | } 81 | else if (options.version) { 82 | cmd.print ( 83 | "%s version %s%s\n", 84 | APP_NAME, 85 | VERSION, 86 | #if BLACKBOX_IS_FLATPAK 87 | " (flatpak)" 88 | #else 89 | "" 90 | #endif 91 | ); 92 | } 93 | else { 94 | new Window ( 95 | this, 96 | options.command, 97 | options.current_working_dir, 98 | false 99 | ).show (); 100 | } 101 | this.release (); 102 | return 0; 103 | } 104 | 105 | // private void on_app_quit () { 106 | // // This involves confirming before closing tabs/windows 107 | // warning ("App quit is not implemented yet."); 108 | // } 109 | 110 | private void on_about () { 111 | var win = create_about_dialog (); 112 | win.present (this.get_active_window ()); 113 | } 114 | 115 | private void on_new_window () { 116 | // TODO: this method has an issue: if the current active window is not a 117 | // main window, the check will fail and the new window will not persist the 118 | // CWD. An alternative solution would be to keep track of the last active 119 | // main window. 120 | var w = this.get_active_window (); 121 | Terminal? active_terminal = (w is Window) ? w.active_terminal : null; 122 | 123 | string? cwd = Terminal 124 | .get_current_working_directory_for_new_session (active_terminal); 125 | 126 | new Window (this, null, cwd, false).show (); 127 | } 128 | 129 | private void on_focus_next_tab () { 130 | (this.get_active_window () as Window)?.focus_next_tab (); 131 | } 132 | 133 | private void on_focus_previous_tab () { 134 | (this.get_active_window () as Window)?.focus_previous_tab (); 135 | } 136 | 137 | private void on_focus_tab (uint window_id, uint tab_id) { 138 | foreach (var _window in this.get_windows()) { 139 | var window = _window as Window; 140 | if (window != null && window.id == window_id) { 141 | window.focus_tab_with_id (tab_id); 142 | return; 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/services/Settings.vala: -------------------------------------------------------------------------------- 1 | /* Settings.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 | 19 | public enum Terminal.ScrollbackMode { 20 | FIXED = 0, 21 | UNLIMITED = 1, 22 | DISABLED = 2, 23 | } 24 | 25 | public enum Terminal.WorkingDirectoryMode { 26 | PREVIOUS_SESSION = 0, 27 | HOME_DIRECTORY = 1, 28 | CUSTOM = 2, 29 | } 30 | 31 | public enum Terminal.ApplicationStyle { 32 | SYSTEM = 0, 33 | LIGHT = 1, 34 | DARK = 2, 35 | } 36 | 37 | public class Terminal.Settings : SettingsBinder { 38 | public bool command_as_login_shell { get; set; } 39 | public bool context_aware_header_bar { get; set; } 40 | public bool easy_copy_paste { get; set; } 41 | public bool fill_tabs { get; set; } 42 | public bool headerbar_drag_area { get; set; } 43 | public bool notify_process_completion { get; set; } 44 | public bool pretty { get; set; } 45 | public bool remember_window_size { get; set; } 46 | public bool show_headerbar { get; set; } 47 | public bool show_menu_button { get; set; } 48 | public bool show_scrollbars { get; set; } 49 | public bool terminal_bell { get; set; } 50 | public bool theme_bold_is_bright { get; set; } 51 | public bool terminal_hide_cursor_while_typing { get; set; } 52 | public bool use_custom_command { get; set; } 53 | public bool use_overlay_scrolling { get; set; } 54 | public bool use_sixel { get; set; } 55 | public bool was_fullscreened { get; set; } 56 | public bool was_maximized { get; set; } 57 | public double terminal_cell_height { get; set; } 58 | public double terminal_cell_width { get; set; } 59 | public string custom_shell_command { get; set; } 60 | public string custom_working_directory { get; set; } 61 | public string font { get; set; } 62 | public string theme_dark { get; set; } 63 | public string theme_light { get; set; } 64 | public uint cursor_blink_mode { get; set; } 65 | public uint cursor_shape { get; set; } 66 | public uint opacity { get; set; } 67 | public uint scrollback_lines { get; set; } 68 | public uint scrollback_mode { get; set; } 69 | public uint style_preference { get; set; } 70 | public uint window_height { get; set; } 71 | public uint window_width { get; set; } 72 | public uint working_directory_mode { get; set; } 73 | public Variant terminal_padding { get; set; } 74 | 75 | public bool floating_controls { get; set; } 76 | public uint floating_controls_hover_area { get; set; } 77 | public uint delay_before_showing_floating_controls { get; set; } 78 | 79 | private static Settings instance = null; 80 | 81 | private Settings () { 82 | base ("com.raggesilver.BlackBox"); 83 | } 84 | 85 | public static Settings get_default () { 86 | if (Settings.instance == null) { 87 | Settings.instance = new Settings (); 88 | } 89 | return Settings.instance; 90 | } 91 | 92 | public Padding get_padding () { 93 | return Padding.from_variant (this.schema.get_value ("terminal-padding")); 94 | } 95 | 96 | public void set_padding (Padding padding) { 97 | this.terminal_padding = padding.to_variant (); 98 | } 99 | } 100 | 101 | public class Terminal.SearchSettings : SettingsBinder { 102 | public bool match_case_sensitive { get; set; } 103 | public bool match_whole_words { get; set; } 104 | public bool match_regex { get; set; } 105 | public bool wrap_around { get; set; } 106 | 107 | private static SearchSettings instance = null; 108 | 109 | private SearchSettings () { 110 | base ("com.raggesilver.BlackBox.terminal.search"); 111 | } 112 | 113 | public static SearchSettings get_default () { 114 | if (SearchSettings.instance == null) { 115 | SearchSettings.instance = new SearchSettings (); 116 | } 117 | return SearchSettings.instance; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/widgets/ShortcutDialog.vala: -------------------------------------------------------------------------------- 1 | /* ShortcutDialog.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/shortcut-dialog.ui")] 22 | public class Terminal.ShortcutDialog : Adw.Dialog { 23 | 24 | static Gtk.KeyvalTrigger JUST_ESCAPE = new Gtk.KeyvalTrigger ( 25 | Gdk.Key.Escape, 26 | 0 27 | ); 28 | 29 | static Gtk.KeyvalTrigger JUST_BACKSPACE = new Gtk.KeyvalTrigger ( 30 | Gdk.Key.BackSpace, 31 | 0 32 | ); 33 | 34 | public signal void response (Gtk.ResponseType response); 35 | public signal void shortcut_set (string? shortcut); 36 | 37 | public string? shortcut_name { get; set; default = null; } 38 | public string? current_accel { get; set; default = null; } 39 | public string? new_accel { get; private set; default = null; } 40 | public bool is_in_use { get; private set; default = false; } 41 | public bool is_shortcut_set { get; protected set; default = false; } 42 | 43 | public string heading_text { 44 | owned get { 45 | return this.shortcut_name == null 46 | ? _("Enter new shortcut") 47 | : _("Enter new shortcut for \"%s\"").printf (this.shortcut_name); 48 | } 49 | } 50 | 51 | [GtkChild] unowned Gtk.ShortcutLabel shortcut_label; 52 | 53 | construct { 54 | if (DEVEL) { 55 | this.add_css_class ("devel"); 56 | } 57 | 58 | this.response.connect_after (() => this.close ()); 59 | 60 | this.notify ["shortcut-name"].connect (() => { 61 | this.notify_property ("heading-text"); 62 | }); 63 | 64 | // bind this.current-accel -> this.shortcut_label.accelerator 65 | this.bind_property ( 66 | "current-accel", 67 | this.shortcut_label, 68 | "accelerator", 69 | BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE, 70 | // current-accel -> accelerator 71 | (_binding, from_value, ref to_value_ref) => { 72 | string? current = from_value.get_string (); 73 | to_value_ref = current ?? ""; 74 | return true; 75 | }, 76 | null 77 | ); 78 | 79 | this.setup_key_controller (); 80 | } 81 | 82 | private void setup_key_controller () { 83 | var kpc = new Gtk.EventControllerKey (); 84 | 85 | kpc.key_pressed.connect ((event, keyval, _keycode, modifier) => { 86 | const Gdk.ModifierType valid_modifiers = 87 | Gdk.ModifierType.CONTROL_MASK | 88 | Gdk.ModifierType.SHIFT_MASK | 89 | Gdk.ModifierType.ALT_MASK; 90 | 91 | var real_modifiers = valid_modifiers & modifier; 92 | 93 | var k = new Gtk.KeyvalTrigger (keyval, real_modifiers); 94 | 95 | if (k.compare (JUST_ESCAPE) == 0) { 96 | this.cancel (); 97 | return false; 98 | } 99 | else if (k.compare (JUST_BACKSPACE) == 0) { 100 | this.shortcut_set (null); 101 | this.apply (); 102 | return true; 103 | } 104 | 105 | uint accel_key = 0; 106 | Gdk.ModifierType mods = 0; 107 | 108 | bool is_valid_gtk_accel = Gtk.accelerator_parse ( 109 | k.to_string (), 110 | out accel_key, 111 | out mods 112 | ); 113 | 114 | Gdk.ModifierType consumed = (event.get_current_event () as Gdk.KeyEvent)? 115 | .get_consumed_modifiers () ?? 0; 116 | 117 | mods &= ~consumed; 118 | 119 | bool is_valid = 120 | is_valid_gtk_accel && 121 | // This is a very stupid way to check if the keyval is not Control_L, 122 | // Shift_L, or Alt_L. We don't want these keys to be valid. 123 | !Gdk.keyval_name (keyval).has_suffix ("_L") && 124 | !Gdk.keyval_name (keyval).has_suffix ("_R") && 125 | // Unless keyval is one of the Function keys, shortcuts need to have a 126 | // modifier. 127 | ( 128 | (keyval >= Gdk.Key.F1 && keyval <= Gdk.Key.F35) || (mods > 0) 129 | ); 130 | 131 | this.shortcut_label.set_accelerator (k.to_string ()); 132 | 133 | // TODO: since we can get the name of the action that is currently using 134 | // this shortcut we should show it to the user 135 | // 136 | // E.g.: This shortcut is currently assigned to "Reset Zoom" 137 | this.is_in_use = Keymap 138 | .get_default () 139 | .get_action_for_shortcut (k.to_string ()) != null; 140 | 141 | this.is_shortcut_set = is_valid && !this.is_in_use; 142 | this.shortcut_set (this.is_shortcut_set ? k.to_string () : null); 143 | 144 | return true; 145 | }); 146 | 147 | (this as Gtk.Widget)?.add_controller (kpc); 148 | } 149 | 150 | [GtkCallback] 151 | void cancel () { 152 | this.response (Gtk.ResponseType.CANCEL); 153 | } 154 | 155 | [GtkCallback] 156 | void apply () { 157 | this.response (Gtk.ResponseType.APPLY); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/utils/SettingsBinder.vala: -------------------------------------------------------------------------------- 1 | public abstract class Terminal.SettingsBinder : Object { 2 | public GLib.Settings schema { get; construct set; } 3 | 4 | private bool updating = false; 5 | private bool doing_setup = true; 6 | 7 | protected SettingsBinder (string path) { 8 | Object(schema: new GLib.Settings(path)); 9 | } 10 | 11 | // Runs after Settings() 12 | construct { 13 | debug("Started settings from '%s'", this.schema.schema_id); 14 | 15 | var obj_class = (ObjectClass) this.get_type().class_ref(); 16 | var properties = obj_class.list_properties(); 17 | 18 | foreach (var prop in properties) { 19 | this.load_key(prop.name); 20 | } 21 | 22 | this.doing_setup = false; 23 | this.schema.changed.connect(this.load_key); 24 | } 25 | 26 | private void load_key (string key) { 27 | if (key == "schema") { 28 | return; 29 | } 30 | 31 | var obj_class = (ObjectClass) this.get_type().class_ref(); 32 | var prop = obj_class.find_property(key); 33 | 34 | if (prop == null) { 35 | return; 36 | } 37 | 38 | this.notify.disconnect(this.on_notify); 39 | 40 | var type = prop.value_type; 41 | var val = Value(type); 42 | this.get_property(key, ref val); 43 | 44 | // Unsupported type 45 | if (val.type() != prop.value_type) { 46 | warning("Unsupported type %s for key %s", type.to_string(), key); 47 | this.notify.connect(this.on_notify); 48 | return; 49 | } 50 | 51 | if (type == typeof(int)) { 52 | this.set_property(prop.name, schema.get_int(key)); 53 | } 54 | else if (type == typeof(uint)) { 55 | this.set_property(prop.name, schema.get_uint(key)); 56 | } 57 | else if (type == typeof(double)) { 58 | this.set_property(prop.name, schema.get_double(key)); 59 | } 60 | else if (type == typeof(bool)) { 61 | this.set_property(prop.name, schema.get_boolean(key)); 62 | } 63 | else if (type == typeof(string)) { 64 | this.set_property(prop.name, schema.get_string(key)); 65 | } 66 | else if (type == typeof(string[])) { 67 | this.set_property(prop.name, schema.get_strv(key)); 68 | } 69 | else if (type == typeof(int64)) { 70 | this.set_property(prop.name, schema.get_value(key).get_int64()); 71 | } 72 | else if (type == typeof(uint64)) { 73 | this.set_property( 74 | prop.name, 75 | schema.get_value(key).get_uint64() 76 | ); 77 | } 78 | else if (type.is_enum()) { 79 | this.set_property(prop.name, schema.get_enum(key)); 80 | } 81 | else if (type == typeof(Variant)) { 82 | this.set_property (prop.name, schema.get_value (key)); 83 | } 84 | 85 | this.notify.connect(this.on_notify); 86 | } 87 | 88 | private void save_key (string key) { 89 | if (key == "schema" || this.updating) { 90 | return; 91 | } 92 | 93 | var obj_class = (ObjectClass) this.get_type().class_ref(); 94 | var prop = obj_class.find_property(key); 95 | 96 | if (prop == null) { 97 | return; 98 | } 99 | 100 | this.notify.disconnect(this.on_notify); 101 | 102 | bool res = true; 103 | this.updating = true; 104 | 105 | var type = prop.value_type; 106 | var val = Value(type); 107 | this.get_property(prop.name, ref val); 108 | 109 | // Unsupported type 110 | if (val.type() != prop.value_type) { 111 | warning("Unsupported type %s for key %s", type.to_string(), key); 112 | } 113 | 114 | if ( 115 | type == typeof(int) && val.get_int() != schema.get_int(key) 116 | ) { 117 | res = this.schema.set_int(key, val.get_int()); 118 | } 119 | else if ( 120 | type == typeof(uint) && val.get_uint() != schema.get_uint(key) 121 | ) { 122 | res = this.schema.set_uint(key, val.get_uint()); 123 | } 124 | else if ( 125 | type == typeof(double) && 126 | val.get_double() != schema.get_double(key) 127 | ) { 128 | res = this.schema.set_double(key, val.get_double()); 129 | } 130 | else if ( 131 | type == typeof(bool) && 132 | val.get_boolean() != schema.get_boolean(key) 133 | ) { 134 | res = this.schema.set_boolean(key, val.get_boolean()); 135 | } 136 | else if ( 137 | type == typeof(string) && 138 | val.get_string() != schema.get_string(key) 139 | ) { 140 | res = this.schema.set_string(key, val.get_string()); 141 | } 142 | else if (type == typeof(string[])) { 143 | string[] strv = null; 144 | this.get(key, &strv); 145 | if (strv != this.schema.get_strv(key)) { 146 | res = this.schema.set_strv(key, strv); 147 | } 148 | } 149 | else if ( 150 | type == typeof(int64) && 151 | val.get_int64() != schema.get_value(key).get_int64() 152 | ) { 153 | res = this.schema.set_value( 154 | key, 155 | new Variant.int64(val.get_int64()) 156 | ); 157 | } 158 | else if ( 159 | type == typeof(uint64) && 160 | val.get_uint64() != schema.get_value(key).get_uint64() 161 | ) { 162 | res = this.schema.set_value( 163 | key, 164 | new Variant.uint64(val.get_uint64()) 165 | ); 166 | } 167 | else if ( 168 | type.is_enum() && val.get_enum() != this.schema.get_enum(key) 169 | ) { 170 | res = this.schema.set_enum(key, val.get_enum()); 171 | } 172 | else if (type == typeof(Variant)) { 173 | res = this.schema.set_value (key, val.get_variant ()); 174 | } 175 | 176 | if (!res) { 177 | warning("Could not update %s", key); 178 | } 179 | 180 | this.updating = false; 181 | this.notify.connect(this.on_notify); 182 | } 183 | 184 | private void on_notify (Object sender, ParamSpec prop) { 185 | this.save_key(prop.name); 186 | } 187 | } 188 | 189 | -------------------------------------------------------------------------------- /src/widgets/AboutDialog.vala: -------------------------------------------------------------------------------- 1 | /* AboutDialog.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 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | namespace Terminal { 22 | public Adw.AboutDialog create_about_dialog () { 23 | var window = new Adw.AboutDialog () { 24 | developer_name = "Paulo Queiroz", 25 | copyright = "© 2022-2023 Paulo Queiroz", 26 | license_type = Gtk.License.GPL_3_0, 27 | application_icon = APP_ID, 28 | application_name = APP_NAME, 29 | version = VERSION, 30 | website = "https://gitlab.gnome.org/raggesilver/blackbox", 31 | issue_url = "https://gitlab.gnome.org/raggesilver/blackbox/-/issues", 32 | debug_info = get_debug_information (), 33 | release_notes = """ 34 |

The Sandbox Conundrum, patch 1.

35 |

Features

36 |
    37 |
  • 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 | 16 | 41 | 52 | 53 | 55 | 59 | 65 | 71 | 77 | 83 | 89 | 95 | 101 | 107 | 113 | 119 | 125 | 131 | 137 | 142 | 147 | 152 | 157 | 163 | 168 | 174 | 179 | 185 | 191 | 197 | 198 | 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 | --------------------------------------------------------------------------------