Skip to content
API Documentation
BastionGuard™
SmbShareDialog.vala
Go to the documentation of this file.
1/*
2 * SmbShareDialog.vala
3 *
4 * Copyright 2025-2026 Calogero Scarnà — BastionGuard Backup
5 *
6 * GPL-2.0-or-later
7 *
8 * Modal window used to add (or edit) an SMB/CIFS network share that
9 * BastionGuard Backup can then use as a snapshot location.
10 *
11 * Implemented as a plain Gtk.Window (not Gtk.Dialog) so that the
12 * project's custom CSS styles it consistently — the GTK4 native
13 * dialog headerbar would otherwise show un-themed Cancel / Connect
14 * buttons overlapping the title.
15 */
16
17using Gtk;
18using Gee;
19
20using TeeJee.Logging;
23using TeeJee.Misc;
24
25public class SmbShareDialog : Gtk.Window {
26
27 private Gtk.Entry entry_server;
28 private Gtk.Entry entry_share;
29 private Gtk.Entry entry_subpath;
30 private Gtk.Entry entry_user;
31 private Gtk.Entry entry_password;
32 private Gtk.Entry entry_domain;
33 private Gtk.Entry entry_label;
34 private Gtk.CheckButton chk_anonymous;
35 private Gtk.CheckButton chk_remember;
36 private Gtk.Label lbl_status;
37 private Gtk.Button btn_connect;
38 private Gtk.Button btn_cancel;
39
40 public SmbShare? result_share = null;
41
42 private GLib.MainLoop? loop = null;
43 private bool ok_pressed = false;
44
45 public SmbShareDialog(Gtk.Window? parent, SmbShare? prefill = null){
46 title = _("Add Network Share (SMB / NAS)");
47
48 if (parent != null){
49 set_transient_for(parent);
50 set_modal(true);
51 }
52 set_default_size(520, -1);
53 resizable = false;
54
55 add_css_class("smb-dialog");
56
57 build_ui();
58
59 if (prefill != null){
60 entry_server.text = prefill.server;
61 entry_share.text = prefill.share;
62 entry_subpath.text = prefill.subpath;
63 entry_user.text = prefill.username;
64 entry_domain.text = prefill.domain;
65 entry_label.text = prefill.label;
66 chk_anonymous.active = (prefill.username.length == 0);
68 }
69
70 // Pressing the close (X) button cancels.
71 close_request.connect(() => {
72 ok_pressed = false;
73 quit_loop();
74 return false;
75 });
76 }
77
78 private void build_ui(){
79 var outer = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
80 outer.margin_start = 22;
81 outer.margin_end = 22;
82 outer.margin_top = 18;
83 outer.margin_bottom = 16;
84 outer.spacing = 8;
85 set_child(outer);
86
87 // Title
88 var title_label = new Gtk.Label(_("Add Network Share (SMB / NAS)"));
89 title_label.xalign = 0.0f;
90 title_label.add_css_class("smb-title");
91 outer.append(title_label);
92
93 // Subtitle / hint
94 var intro = new Gtk.Label(
95 _("Enter the address of your NAS or shared folder.")
96 + "\n"
97 + _("Example: server = nas.local, share = backups"));
98 intro.xalign = 0.0f;
99 intro.wrap = true;
100 intro.wrap_mode = Pango.WrapMode.WORD;
101 intro.add_css_class("smb-subtitle");
102 outer.append(intro);
103
104 var grid = new Gtk.Grid();
105 grid.row_spacing = 8;
106 grid.column_spacing = 12;
107 grid.margin_top = 4;
108 outer.append(grid);
109
110 int row = 0;
111
112 entry_server = add_row(grid, row++, _("Server") + " *", _("e.g. nas.local or 192.168.1.10"));
113 entry_share = add_row(grid, row++, _("Share") + " *", _("e.g. backups"));
114 entry_subpath = add_row(grid, row++, _("Sub-folder"), _("Optional — path inside the share"));
115 entry_label = add_row(grid, row++, _("Label"), _("Optional — friendly name shown in the list"));
116
117 // Separator
118 var sep = new Gtk.Separator(Gtk.Orientation.HORIZONTAL);
119 sep.margin_top = 8;
120 sep.margin_bottom = 6;
121 grid.attach(sep, 0, row++, 2, 1);
122
123 // Anonymous toggle
124 chk_anonymous = new Gtk.CheckButton.with_label(_("Connect anonymously (guest access)"));
125 chk_anonymous.toggled.connect(update_anonymous_state);
126 grid.attach(chk_anonymous, 0, row++, 2, 1);
127
128 entry_user = add_row(grid, row++, _("Username"), "");
129 entry_password = add_row(grid, row++, _("Password"), "");
130 entry_password.visibility = false;
131 entry_password.input_purpose = Gtk.InputPurpose.PASSWORD;
132 entry_domain = add_row(grid, row++, _("Domain / Workgroup"), _("Optional"));
133
134 var sep2 = new Gtk.Separator(Gtk.Orientation.HORIZONTAL);
135 sep2.margin_top = 8;
136 sep2.margin_bottom = 6;
137 grid.attach(sep2, 0, row++, 2, 1);
138
139 chk_remember = new Gtk.CheckButton.with_label(
140 _("Remember this share (the password is never saved)"));
141 chk_remember.active = true;
142 grid.attach(chk_remember, 0, row++, 2, 1);
143
144 // Status label (shown on errors)
145 lbl_status = new Gtk.Label("");
146 lbl_status.xalign = 0.0f;
147 lbl_status.wrap = true;
148 lbl_status.wrap_mode = Pango.WrapMode.WORD;
149 lbl_status.visible = false;
150 lbl_status.margin_top = 8;
151 lbl_status.add_css_class("error-text");
152 outer.append(lbl_status);
153
154 // Action buttons (own row, right-aligned)
155 var btn_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
156 btn_box.halign = Gtk.Align.END;
157 btn_box.margin_top = 14;
158 outer.append(btn_box);
159
160 btn_cancel = new Gtk.Button.with_label(_("Cancel"));
161 btn_cancel.add_css_class("btn-cancel");
162 btn_cancel.clicked.connect(() => {
163 ok_pressed = false;
164 quit_loop();
165 });
166 btn_box.append(btn_cancel);
167
168 btn_connect = new Gtk.Button.with_label(_("Connect"));
169 btn_connect.add_css_class("btn-primary");
170 btn_connect.clicked.connect(on_connect_clicked);
171 btn_box.append(btn_connect);
172
173 // Pressing Enter inside any entry triggers Connect.
174 entry_server.activate.connect(() => { btn_connect.activate(); });
175 entry_share.activate.connect(() => { btn_connect.activate(); });
176 entry_subpath.activate.connect(() => { btn_connect.activate(); });
177 entry_user.activate.connect(() => { btn_connect.activate(); });
178 entry_password.activate.connect(() => { btn_connect.activate(); });
179 entry_domain.activate.connect(() => { btn_connect.activate(); });
180 entry_label.activate.connect(() => { btn_connect.activate(); });
181 }
182
183 private Gtk.Entry add_row(Gtk.Grid grid, int row, string label_text, string placeholder){
184 var lbl = new Gtk.Label(label_text);
185 lbl.xalign = 0.0f;
186 lbl.halign = Gtk.Align.START;
187 lbl.valign = Gtk.Align.CENTER;
188 grid.attach(lbl, 0, row, 1, 1);
189
190 var entry = new Gtk.Entry();
191 entry.hexpand = true;
192 if (placeholder.length > 0){
193 entry.placeholder_text = placeholder;
194 }
195 grid.attach(entry, 1, row, 1, 1);
196 return entry;
197 }
198
200 bool anon = chk_anonymous.active;
201 entry_user.sensitive = !anon;
202 entry_password.sensitive = !anon;
203 entry_domain.sensitive = !anon;
204 if (anon){
205 entry_user.text = "";
206 entry_password.text = "";
207 entry_domain.text = "";
208 }
209 }
210
211 private void show_error(string text){
212 lbl_status.label = text;
213 lbl_status.visible = true;
214 }
215
216 private void on_connect_clicked(){
217 // Build candidate
218 var s = new SmbShare();
219 s.server = entry_server.text.strip();
220 s.share = entry_share.text.strip();
221 s.subpath = entry_subpath.text.strip().replace("\\", "/").strip();
222 s.username = entry_user.text.strip();
223 s.password = entry_password.text;
224 s.domain = entry_domain.text.strip();
225 s.label = entry_label.text.strip();
226 s.save_credentials = chk_remember.active;
227
228 // Trim a leading "//" or "smb://" pasted by the user
229 if (s.server.has_prefix("smb://")){
230 s.server = s.server.substring(6);
231 }
232 while (s.server.has_prefix("/")){
233 s.server = s.server.substring(1);
234 }
235 // If the user pasted server/share into the server box, split it.
236 if (s.share.length == 0 && s.server.contains("/")){
237 int idx = s.server.index_of("/");
238 s.share = s.server.substring(idx + 1);
239 s.server = s.server.substring(0, idx);
240 }
241
242 if (s.server.length == 0 || s.share.length == 0){
243 show_error(_("Please fill in both Server and Share."));
244 return;
245 }
246
247 // Try to mount
248 string err;
249 btn_connect.sensitive = false;
250 btn_cancel.sensitive = false;
251 gtk_set_busy(true, this);
252
253 string mp = SmbMount.mount(s, out err);
254
255 gtk_set_busy(false, this);
256 btn_connect.sensitive = true;
257 btn_cancel.sensitive = true;
258
259 if (mp.length == 0){
260 show_error(err);
261 return;
262 }
263
264 // Persist (without password)
265 if (s.save_credentials){
267 }
268
269 result_share = s;
270 ok_pressed = true;
271 quit_loop();
272 }
273
274 private void quit_loop(){
275 if (loop != null && loop.is_running()){
276 loop.quit();
277 }
278 this.hide();
279 }
280
281 /**
282 * Convenience: opens the window modally and returns the resulting
283 * SmbShare (or null if the user cancelled / the mount failed).
284 */
285 public static SmbShare? run_dialog(Gtk.Window? parent, SmbShare? prefill = null){
286 var dlg = new SmbShareDialog(parent, prefill);
287 dlg.loop = new GLib.MainLoop(null, false);
288 dlg.present();
289 dlg.loop.run();
290 var r = dlg.ok_pressed ? dlg.result_share : null;
291 dlg.destroy();
292 return r;
293 }
294}
#define _(s)
static string mount(SmbShare share, out string error_message)
static void save_share(SmbShare share)
Gtk.Entry entry_password
Gtk.Button btn_connect
void update_anonymous_state()
SmbShare? result_share
SmbShareDialog(Gtk.Window? parent, SmbShare? prefill=null)
Gtk.Entry entry_subpath
Gtk.CheckButton chk_remember
Gtk.Button btn_cancel
static ? SmbShare run_dialog(Gtk.Window? parent, SmbShare? prefill=null)
Gtk.Entry entry_share
Gtk.Entry add_row(Gtk.Grid grid, int row, string label_text, string placeholder)
GLib.? MainLoop loop
Gtk.Entry entry_domain
void show_error(string text)
Gtk.Entry entry_server
Gtk.Entry entry_label
Gtk.CheckButton chk_anonymous
void gtk_set_busy(bool busy, Gtk.Window win)