diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..604652f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "bindings/vapi"]
+ path = bindings/vapi
+ url = https://gitlab.gnome.org/GNOME/vala-extra-vapis.git
diff --git a/bindings/vapi b/bindings/vapi
new file mode 160000
index 0000000..9ce7531
--- /dev/null
+++ b/bindings/vapi
@@ -0,0 +1 @@
+Subproject commit 9ce7531a398c3ba629c89aad6ba43fb73c6060b6
diff --git a/fr.oupson.FooTerm.json b/fr.oupson.FooTerm.json
index 64ee555..d88f4d4 100644
--- a/fr.oupson.FooTerm.json
+++ b/fr.oupson.FooTerm.json
@@ -37,13 +37,33 @@
"buildsystem": "meson",
"config-opts": ["-Dgtk4=true", "-Dgtk3=false", "-Dsixel=true"],
"sources": [
- {
- "type": "archive",
- "url": "https://gitlab.gnome.org/GNOME/vte/-/archive/015ca4d2fdc57b625add7b23b0afa7193adc45a9/vte-015ca4d2fdc57b625add7b23b0afa7193adc45a9.tar.gz",
- "sha256": "ba918109936692fe555f1b28f428680ed4940a350709bcf908bdbf9a93498c08"
- }
+ {
+ "type": "archive",
+ "url": "https://gitlab.gnome.org/GNOME/vte/-/archive/015ca4d2fdc57b625add7b23b0afa7193adc45a9/vte-015ca4d2fdc57b625add7b23b0afa7193adc45a9.tar.gz",
+ "sha256": "ba918109936692fe555f1b28f428680ed4940a350709bcf908bdbf9a93498c08"
+ }
]
},
+ {
+ "name" : "libssh2",
+ "buildsystem" : "cmake-ninja",
+ "config-opts" : [
+ "-DCMAKE_BUILD_TYPE=RelWithDebInfo",
+ "-DCMAKE_INSTALL_LIBDIR:PATH=/app/lib",
+ "-DBUILD_SHARED_LIBS:BOOL=ON"
+ ],
+ "cleanup" : [
+ "/share/doc"
+ ],
+ "sources" : [
+ {
+ "type" : "git",
+ "url" : "https://github.com/libssh2/libssh2.git",
+ "tag" : "libssh2-1.10.0"
+ }
+ ]
+ },
+
{
"name" : "footerm",
"builddir" : true,
diff --git a/meson.build b/meson.build
index fa5e1ca..25c8db1 100644
--- a/meson.build
+++ b/meson.build
@@ -7,7 +7,17 @@ project('footerm', ['c', 'vala'],
i18n = import('i18n')
gnome = import('gnome')
+metadata_dir = meson.project_source_root() / 'bindings'/ 'metadata'
+vapi_dir = meson.project_source_root() / 'bindings' / 'vapi'
+add_project_arguments([
+ # Make sure Meson can find custom VAPIs
+ '--vapidir', vapi_dir,
+ '--metadatadir', metadata_dir,
+ ],
+ language: 'vala'
+)
+add_project_arguments('-D_GNU_SOURCE', language: 'c')
subdir('data')
subdir('src')
diff --git a/src/footerm.gresource.xml b/src/footerm.gresource.xml
index f1dfa8a..c923920 100644
--- a/src/footerm.gresource.xml
+++ b/src/footerm.gresource.xml
@@ -2,6 +2,9 @@
window.ui
+ newpane.ui
+ terminalpane.ui
+ newserver.ui
gtk/help-overlay.ui
diff --git a/src/main.vala b/src/main.vala
index 0cc9227..fc21864 100644
--- a/src/main.vala
+++ b/src/main.vala
@@ -19,6 +19,15 @@
*/
int main (string[] args) {
+ var rc = SSH2.init (0);
+ if (rc != SSH2.Error.NONE) {
+ stdout.printf ("libssh2 initialization failed (%d)\n", rc);
+ return -1;
+ }
+
var app = new Footerm.Application ();
- return app.run (args);
+ var res = app.run(args);
+
+ SSH2.exit ();
+ return res;
}
diff --git a/src/meson.build b/src/meson.build
index 91d60de..f8f946b 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -2,14 +2,21 @@ footerm_sources = [
'main.vala',
'application.vala',
'window.vala',
+ 'newpane.vala',
+ 'newserver.vala',
+ 'terminalpane.vala',
]
footerm_deps = [
dependency('gtk4'),
dependency('libadwaita-1'),
dependency('vte-2.91-gtk4', version: '>= 0.70.0'),
+ dependency('json-glib-1.0', version: '>= 1.2.0'),
+ dependency('libssh2'),
]
+subdir('model')
+
footerm_sources += gnome.compile_resources('footerm-resources',
'footerm.gresource.xml',
c_name: 'footerm'
diff --git a/src/model/Server.vala b/src/model/Server.vala
new file mode 100644
index 0000000..1e4bceb
--- /dev/null
+++ b/src/model/Server.vala
@@ -0,0 +1,35 @@
+/* Server.vala
+ *
+ * Copyright 2023 oupson
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+namespace FooTerm.Model {
+ public class Server {
+ public string hostname;
+ public ushort port;
+ public string username;
+ public string password;
+
+ public Server(string hostname, ushort port, string username, string password) {
+ this.hostname = hostname;
+ this.port = port;
+ this.username = username;
+ this.password = password;
+ }
+ }
+}
diff --git a/src/model/meson.build b/src/model/meson.build
new file mode 100644
index 0000000..933b095
--- /dev/null
+++ b/src/model/meson.build
@@ -0,0 +1,3 @@
+footerm_sources += files(
+ 'Server.vala'
+)
\ No newline at end of file
diff --git a/src/newpane.ui b/src/newpane.ui
new file mode 100644
index 0000000..219c104
--- /dev/null
+++ b/src/newpane.ui
@@ -0,0 +1,45 @@
+
+
+
+
+
+
diff --git a/src/newpane.vala b/src/newpane.vala
new file mode 100644
index 0000000..c5515d2
--- /dev/null
+++ b/src/newpane.vala
@@ -0,0 +1,71 @@
+/* newpane.vala
+ *
+ * Copyright 2023 oupson
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+namespace FooTerm {
+ [GtkTemplate (ui = "/fr/oupson/FooTerm/newpane.ui")]
+ public class NewPane : Gtk.Box {
+ [GtkChild]
+ private unowned Adw.PreferencesGroup server_list;
+
+ [GtkChild]
+ private unowned FooTerm.NewServer new_server;
+
+ [GtkChild]
+ private unowned Gtk.Stack newpane_stack;
+
+ [GtkChild]
+ private unowned Gtk.Button newpane_add_button;
+
+ public signal void on_server_selected(FooTerm.Model.Server server);
+
+ construct {
+ this.new_server.on_new_server.connect((s) => {
+ this.newpane_stack.set_visible_child(server_list.get_parent());
+ var action_row = new Adw.ActionRow();
+ action_row.set_title(s.hostname);
+ action_row.set_activatable(true);
+ action_row.activated.connect(() => {
+ this.on_server_selected(s);
+ });
+ server_list.add(action_row);
+ });
+ this.newpane_add_button.clicked.connect (() => {
+ this.newpane_stack.set_visible_child(new_server.get_parent());
+ });
+
+ try {
+ var config_dir = GLib.File.new_for_path(GLib.Environment.get_user_config_dir());
+ var server_files = config_dir.get_child("servers.json");
+
+ if (server_files.query_exists()) {
+ var parser = new Json.Parser();
+
+ parser.load_from_file(server_files.get_path());
+
+ // TODO
+ } else {
+ GLib.debug("Server file does not exist");
+ }
+ } catch (Error e) {
+ GLib.warning("Failed to read server list : %s", e.message);
+ }
+ }
+ }
+}
diff --git a/src/newserver.ui b/src/newserver.ui
new file mode 100644
index 0000000..a6d8ed7
--- /dev/null
+++ b/src/newserver.ui
@@ -0,0 +1,50 @@
+
+
+
+
diff --git a/src/newserver.vala b/src/newserver.vala
new file mode 100644
index 0000000..dd193aa
--- /dev/null
+++ b/src/newserver.vala
@@ -0,0 +1,58 @@
+/* newserver.vala
+ *
+ * Copyright 2023 oupson
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+namespace FooTerm {
+ [GtkTemplate (ui = "/fr/oupson/FooTerm/newserver.ui")]
+ public class NewServer : Gtk.Box {
+ public signal void on_new_server(FooTerm.Model.Server server);
+
+ [GtkChild]
+ private unowned Adw.EntryRow hostname_entry;
+
+ [GtkChild]
+ private unowned Adw.EntryRow port_entry;
+
+ [GtkChild]
+ private unowned Adw.EntryRow username_entry;
+
+ [GtkChild]
+ private unowned Adw.PasswordEntryRow password_entry;
+
+ [GtkChild]
+ private unowned Gtk.Button add_server_button;
+
+ construct {
+ add_server_button.clicked.connect (this.on_add_button_clicked);
+ }
+
+ private void on_add_button_clicked() {
+ var hostname = this.hostname_entry.get_text();
+ var port = int.parse(this.port_entry.get_text());
+ var username = this.username_entry.get_text();
+ var password = this.password_entry.get_text();
+
+ if (port > 65545 || port < 0) {
+ // Port is invalid
+ }
+
+ this.on_new_server(new FooTerm.Model.Server(hostname, (ushort)port, username, password));
+ }
+ }
+}
diff --git a/src/terminalpane.ui b/src/terminalpane.ui
new file mode 100644
index 0000000..008c8cb
--- /dev/null
+++ b/src/terminalpane.ui
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/src/terminalpane.vala b/src/terminalpane.vala
new file mode 100644
index 0000000..b1231d4
--- /dev/null
+++ b/src/terminalpane.vala
@@ -0,0 +1,190 @@
+/* terminalpane.vala
+ *
+ * Copyright 2023 oupson
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+namespace FooTerm {
+ [GtkTemplate (ui = "/fr/oupson/FooTerm/terminalpane.ui")]
+ public class TerminalPane : Gtk.Box {
+ [GtkChild]
+ private unowned Vte.Terminal terminal;
+
+ private SSH2.Session? session;
+ private SSH2.Channel? channel;
+ private Socket socket;
+ private int slave_pty;
+
+ private FooTerm.Model.Server server;
+
+ public TerminalPane(FooTerm.Model.Server server) {
+ this.server = server;
+ this.terminal.set_enable_sixel (true);
+ this.connect_to_server();
+ this.terminal.char_size_changed.connect(() => {
+ int rows = 0;
+ int columns = 0;
+ this.terminal.get_pty().get_size(out rows, out columns);
+ this.channel.request_pty_size(columns, rows);
+ });
+ }
+
+ ~TerminalPane() {
+ // TODO DESTRUCT
+ // session.disconnect( "Normal Shutdown, Thank you for playing");
+ // session = null;
+ // Posix.close(sock);
+ // stdout.printf("all done!\n");)
+ }
+
+ private void connect_to_server() throws GLib.IOError, GLib.Error {
+ this.socket = new Socket (SocketFamily.IPV4, SocketType.STREAM, SocketProtocol.TCP);
+ var addrs = new NetworkAddress(this.server.hostname, this.server.port);
+ socket.connect(addrs.enumerate().next(), null);
+
+ var sock = socket.get_fd(); // TODO
+
+ this.session = SSH2.Session.create();
+ if (session.handshake(sock) != SSH2.Error.NONE) {
+ stderr.printf("Failure establishing SSH session\n");
+ return;
+ }
+
+ var fingerprint = session.get_host_key_hash(SSH2.HashType.SHA1);
+ stdout.printf("Fingerprint: ");
+ for(var i = 0; i < 20; i++) {
+ stdout.printf("%02X ", fingerprint[i]);
+ }
+ stdout.printf("\n");
+
+ if (session.auth_password(this.server.username, this.server.password) != SSH2.Error.NONE) {
+ stdout.printf("\tAuthentication by password failed!\n");
+ session.disconnect( "Normal Shutdown, Thank you for playing");
+ session = null;
+ Posix.close(sock);
+ return;
+ } else {
+ stdout.printf("\tAuthentication by password succeeded.\n");
+ }
+
+ this.channel = null;
+ if (session.authenticated && (channel = session.open_channel()) == null) {
+ stderr.printf("Unable to open a session\n");
+ } else {
+ if (channel.request_pty("xterm-256color".data) != SSH2.Error.NONE) {
+ stderr.printf("Failed requesting pty\n");
+ session.disconnect( "Normal Shutdown, Thank you for playing");
+ session = null;
+ Posix.close(sock);
+ }
+
+ channel.set_env ("TERM", "xterm-256color");
+
+ if (channel.start_shell() != SSH2.Error.NONE) {
+ stderr.printf("Unable to request shell on allocated pty\n");
+ session.disconnect( "Normal Shutdown, Thank you for playing");
+ session = null;
+ Posix.close(sock);
+ }
+
+ var master_pty = Posix.posix_openpt(Posix.O_RDWR);
+ if (master_pty == -1) {
+ throw GLib.IOError.from_errno (Posix.errno);
+ }
+
+ var settings = Posix.termios();
+ Posix.cfmakeraw (ref settings);
+
+ if (Posix.tcsetattr (master_pty, Posix.TCSANOW, settings) == -1) {
+ throw GLib.IOError.from_errno (Posix.errno);
+ }
+
+ if (Posix.grantpt(master_pty) == -1) {
+ throw GLib.IOError.from_errno (Posix.errno);
+ }
+
+ if (Posix.unlockpt(master_pty) == -1) {
+ throw GLib.IOError.from_errno (Posix.errno);
+ }
+
+ var pts_name = Posix.ptsname(master_pty);
+ if (pts_name == null) {
+ throw GLib.IOError.from_errno (Posix.errno);
+ }
+
+ this.slave_pty = Posix.open(pts_name, Posix.O_RDWR);
+ if (this.slave_pty < 0) {
+ throw GLib.IOError.from_errno (Posix.errno);
+ }
+
+ var vte_pty = new Vte.Pty.foreign_sync(master_pty, null);
+ this.terminal.set_pty(vte_pty);
+
+ session.blocking = false;
+
+ var sock_channel = new GLib.IOChannel.unix_new(sock);
+ sock_channel.set_encoding (null);
+ sock_channel.set_buffered (false);
+ sock_channel.set_close_on_unref (false);
+
+ var slave_channel = new GLib.IOChannel.unix_new(slave_pty);
+ slave_channel.set_encoding (null);
+ slave_channel.set_buffered (false);
+
+ sock_channel.add_watch (GLib.IOCondition.IN, (source, condition) => {
+ if (condition == IOCondition.HUP) {
+ print ("The connection has been broken.\n");
+ return false;
+ }
+
+ try {
+ var buffer = new uint8[1024];
+ var size = this.channel.read(buffer);
+
+ if (Posix.write(this.slave_pty, buffer, size) < 0) {
+ throw GLib.IOError.from_errno(Posix.errno);
+ }
+
+ return true;
+ } catch(Error e) {
+ GLib.warning("Failed to read from ssh : %s", e.message);
+ return false;
+ }
+ });
+
+ slave_channel.add_watch (GLib.IOCondition.IN, (source, condition) => {
+ if (condition == IOCondition.HUP) {
+ print ("The connection has been broken.\n");
+ return false;
+ }
+
+ try {
+ var buffer = new char[1024];
+ size_t size = 0;
+ source.read_chars(buffer, out size);
+
+ this.channel.write ((uint8[])buffer[0:size]);
+ return true;
+ } catch (Error e) {
+ GLib.warning("Failed to read from terminal : %s", e.message);
+ return false;
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/src/window.ui b/src/window.ui
index ba00f0d..4069947 100644
--- a/src/window.ui
+++ b/src/window.ui
@@ -2,7 +2,6 @@
-