1pub mod connected_el;
2pub mod connected_view;
3pub mod mappings;
4pub mod modal;
5pub mod terminal_view;
6
7use alacritty_terminal::{
8 ansi::{ClearMode, Handler},
9 config::{Config, Program, PtyConfig, Scrolling},
10 event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
11 event_loop::{EventLoop, Msg, Notifier},
12 grid::{Dimensions, Scroll},
13 index::{Direction, Point},
14 selection::{Selection, SelectionType},
15 sync::FairMutex,
16 term::{RenderableContent, TermMode},
17 tty::{self, setup_env},
18 Term,
19};
20use anyhow::{bail, Result};
21
22use futures::{
23 channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
24 FutureExt,
25};
26
27use mappings::mouse::{
28 alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
29};
30use modal::deploy_modal;
31use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
32use std::{collections::HashMap, fmt::Display, ops::Sub, path::PathBuf, sync::Arc, time::Duration};
33use thiserror::Error;
34
35use gpui::{
36 geometry::vector::{vec2f, Vector2F},
37 keymap::Keystroke,
38 ClipboardItem, Entity, ModelContext, MouseButton, MouseButtonEvent, MouseMovedEvent,
39 MutableAppContext, ScrollWheelEvent,
40};
41
42use crate::mappings::{
43 colors::{get_color_at_index, to_alac_rgb},
44 keys::to_esc_str,
45};
46
47///Initialize and register all of our action handlers
48pub fn init(cx: &mut MutableAppContext) {
49 cx.add_action(deploy_modal);
50
51 terminal_view::init(cx);
52 connected_view::init(cx);
53}
54
55///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
56///Scroll multiplier that is set to 3 by default. This will be removed when I
57///Implement scroll bars.
58pub const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
59
60const DEBUG_TERMINAL_WIDTH: f32 = 500.;
61const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
62const DEBUG_CELL_WIDTH: f32 = 5.;
63const DEBUG_LINE_HEIGHT: f32 = 5.;
64
65///Upward flowing events, for changing the title and such
66#[derive(Clone, Copy, Debug)]
67pub enum Event {
68 TitleChanged,
69 CloseTerminal,
70 Bell,
71 Wakeup,
72 BlinkChanged,
73}
74
75#[derive(Clone, Debug)]
76enum InternalEvent {
77 TermEvent(AlacTermEvent),
78 Resize(TerminalSize),
79 Clear,
80 Scroll(Scroll),
81 SetSelection(Option<Selection>),
82 UpdateSelection((Point, Direction)),
83 Copy,
84}
85
86///A translation struct for Alacritty to communicate with us from their event loop
87#[derive(Clone)]
88pub struct ZedListener(UnboundedSender<AlacTermEvent>);
89
90impl EventListener for ZedListener {
91 fn send_event(&self, event: AlacTermEvent) {
92 self.0.unbounded_send(event).ok();
93 }
94}
95
96#[derive(Clone, Copy, Debug)]
97pub struct TerminalSize {
98 cell_width: f32,
99 line_height: f32,
100 height: f32,
101 width: f32,
102}
103
104impl TerminalSize {
105 pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
106 TerminalSize {
107 cell_width,
108 line_height,
109 width: size.x(),
110 height: size.y(),
111 }
112 }
113
114 pub fn num_lines(&self) -> usize {
115 (self.height / self.line_height).floor() as usize
116 }
117
118 pub fn num_columns(&self) -> usize {
119 (self.width / self.cell_width).floor() as usize
120 }
121
122 pub fn height(&self) -> f32 {
123 self.height
124 }
125
126 pub fn width(&self) -> f32 {
127 self.width
128 }
129
130 pub fn cell_width(&self) -> f32 {
131 self.cell_width
132 }
133
134 pub fn line_height(&self) -> f32 {
135 self.line_height
136 }
137}
138impl Default for TerminalSize {
139 fn default() -> Self {
140 TerminalSize::new(
141 DEBUG_LINE_HEIGHT,
142 DEBUG_CELL_WIDTH,
143 vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
144 )
145 }
146}
147
148impl From<TerminalSize> for WindowSize {
149 fn from(val: TerminalSize) -> Self {
150 WindowSize {
151 num_lines: val.num_lines() as u16,
152 num_cols: val.num_columns() as u16,
153 cell_width: val.cell_width() as u16,
154 cell_height: val.line_height() as u16,
155 }
156 }
157}
158
159impl Dimensions for TerminalSize {
160 fn total_lines(&self) -> usize {
161 self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer...
162 }
163
164 fn screen_lines(&self) -> usize {
165 self.num_lines()
166 }
167
168 fn columns(&self) -> usize {
169 self.num_columns()
170 }
171}
172
173#[derive(Error, Debug)]
174pub struct TerminalError {
175 pub directory: Option<PathBuf>,
176 pub shell: Option<Shell>,
177 pub source: std::io::Error,
178}
179
180impl TerminalError {
181 pub fn fmt_directory(&self) -> String {
182 self.directory
183 .clone()
184 .map(|path| {
185 match path
186 .into_os_string()
187 .into_string()
188 .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
189 {
190 Ok(s) => s,
191 Err(s) => s,
192 }
193 })
194 .unwrap_or_else(|| {
195 let default_dir =
196 dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
197 match default_dir {
198 Some(dir) => format!("<none specified, using home directory> {}", dir),
199 None => "<none specified, could not find home directory>".to_string(),
200 }
201 })
202 }
203
204 pub fn shell_to_string(&self) -> Option<String> {
205 self.shell.as_ref().map(|shell| match shell {
206 Shell::System => "<system shell>".to_string(),
207 Shell::Program(p) => p.to_string(),
208 Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
209 })
210 }
211
212 pub fn fmt_shell(&self) -> String {
213 self.shell
214 .clone()
215 .map(|shell| match shell {
216 Shell::System => {
217 let mut buf = [0; 1024];
218 let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
219
220 match pw {
221 Some(pw) => format!("<system defined shell> {}", pw.shell),
222 None => "<could not access the password file>".to_string(),
223 }
224 }
225 Shell::Program(s) => s,
226 Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
227 })
228 .unwrap_or_else(|| {
229 let mut buf = [0; 1024];
230 let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
231 match pw {
232 Some(pw) => {
233 format!("<none specified, using system defined shell> {}", pw.shell)
234 }
235 None => "<none specified, could not access the password file> {}".to_string(),
236 }
237 })
238 }
239}
240
241impl Display for TerminalError {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 let dir_string: String = self.fmt_directory();
244 let shell = self.fmt_shell();
245
246 write!(
247 f,
248 "Working directory: {} Shell command: `{}`, IOError: {}",
249 dir_string, shell, self.source
250 )
251 }
252}
253
254pub struct TerminalBuilder {
255 terminal: Terminal,
256 events_rx: UnboundedReceiver<AlacTermEvent>,
257}
258
259impl TerminalBuilder {
260 pub fn new(
261 working_directory: Option<PathBuf>,
262 shell: Option<Shell>,
263 env: Option<HashMap<String, String>>,
264 initial_size: TerminalSize,
265 blink_settings: Option<TerminalBlink>,
266 alternate_scroll: &AlternateScroll,
267 ) -> Result<TerminalBuilder> {
268 let pty_config = {
269 let alac_shell = shell.clone().and_then(|shell| match shell {
270 Shell::System => None,
271 Shell::Program(program) => Some(Program::Just(program)),
272 Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
273 });
274
275 PtyConfig {
276 shell: alac_shell,
277 working_directory: working_directory.clone(),
278 hold: false,
279 }
280 };
281
282 let mut env = env.unwrap_or_default();
283
284 //TODO: Properly set the current locale,
285 env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
286
287 let alac_scrolling = Scrolling::default();
288 // alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
289
290 let config = Config {
291 pty_config: pty_config.clone(),
292 env,
293 scrolling: alac_scrolling,
294 ..Default::default()
295 };
296
297 setup_env(&config);
298
299 //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
300 //TODO: Remove with a bounded sender which can be dispatched on &self
301 let (events_tx, events_rx) = unbounded();
302 //Set up the terminal...
303 let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
304
305 //Start off blinking if we need to
306 if let Some(TerminalBlink::On) = blink_settings {
307 term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
308 }
309
310 //Start alternate_scroll if we need to
311 if let AlternateScroll::On = alternate_scroll {
312 term.set_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
313 } else {
314 //Alacritty turns it on by default, so we need to turn it off.
315 term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
316 }
317
318 let term = Arc::new(FairMutex::new(term));
319
320 //Setup the pty...
321 let pty = match tty::new(&pty_config, initial_size.into(), None) {
322 Ok(pty) => pty,
323 Err(error) => {
324 bail!(TerminalError {
325 directory: working_directory,
326 shell,
327 source: error,
328 });
329 }
330 };
331
332 let shell_txt = {
333 match shell {
334 Some(Shell::System) | None => {
335 let mut buf = [0; 1024];
336 let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap();
337 pw.shell.to_string()
338 }
339 Some(Shell::Program(program)) => program,
340 Some(Shell::WithArguments { program, args }) => {
341 format!("{} {}", program, args.join(" "))
342 }
343 }
344 };
345
346 //And connect them together
347 let event_loop = EventLoop::new(
348 term.clone(),
349 ZedListener(events_tx.clone()),
350 pty,
351 pty_config.hold,
352 false,
353 );
354
355 //Kick things off
356 let pty_tx = event_loop.channel();
357 let _io_thread = event_loop.spawn();
358
359 let terminal = Terminal {
360 pty_tx: Notifier(pty_tx),
361 term,
362 events: vec![],
363 title: shell_txt.clone(),
364 default_title: shell_txt,
365 last_mode: TermMode::NONE,
366 cur_size: initial_size,
367 last_mouse: None,
368 last_offset: 0,
369 };
370
371 Ok(TerminalBuilder {
372 terminal,
373 events_rx,
374 })
375 }
376
377 pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
378 //Event loop
379 cx.spawn_weak(|this, mut cx| async move {
380 use futures::StreamExt;
381
382 while let Some(event) = self.events_rx.next().await {
383 this.upgrade(&cx)?.update(&mut cx, |this, cx| {
384 //Process the first event immediately for lowered latency
385 this.process_event(&event, cx);
386 });
387
388 'outer: loop {
389 let mut events = vec![];
390 let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
391
392 loop {
393 futures::select_biased! {
394 _ = timer => break,
395 event = self.events_rx.next() => {
396 if let Some(event) = event {
397 events.push(event);
398 if events.len() > 100 {
399 break;
400 }
401 } else {
402 break;
403 }
404 },
405 }
406 }
407
408 if events.is_empty() {
409 smol::future::yield_now().await;
410 break 'outer;
411 } else {
412 this.upgrade(&cx)?.update(&mut cx, |this, cx| {
413 for event in events {
414 this.process_event(&event, cx);
415 }
416 });
417 smol::future::yield_now().await;
418 }
419 }
420 }
421
422 Some(())
423 })
424 .detach();
425
426 self.terminal
427 }
428}
429
430pub struct Terminal {
431 pty_tx: Notifier,
432 term: Arc<FairMutex<Term<ZedListener>>>,
433 events: Vec<InternalEvent>,
434 default_title: String,
435 title: String,
436 cur_size: TerminalSize,
437 last_mode: TermMode,
438 last_offset: usize,
439 last_mouse: Option<(Point, Direction)>,
440}
441
442impl Terminal {
443 fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
444 match event {
445 AlacTermEvent::Title(title) => {
446 self.title = title.to_string();
447 cx.emit(Event::TitleChanged);
448 }
449 AlacTermEvent::ResetTitle => {
450 self.title = self.default_title.clone();
451 cx.emit(Event::TitleChanged);
452 }
453 AlacTermEvent::ClipboardStore(_, data) => {
454 cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
455 }
456 AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
457 &cx.read_from_clipboard()
458 .map(|ci| ci.text().to_string())
459 .unwrap_or_else(|| "".to_string()),
460 )),
461 AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
462 AlacTermEvent::TextAreaSizeRequest(format) => {
463 self.write_to_pty(format(self.cur_size.into()))
464 }
465 AlacTermEvent::CursorBlinkingChange => {
466 cx.emit(Event::BlinkChanged);
467 }
468 AlacTermEvent::Bell => {
469 cx.emit(Event::Bell);
470 }
471 AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
472 AlacTermEvent::MouseCursorDirty => {
473 //NOOP, Handled in render
474 }
475 AlacTermEvent::Wakeup => {
476 cx.emit(Event::Wakeup);
477 cx.notify();
478 }
479 AlacTermEvent::ColorRequest(_, _) => {
480 self.events.push(InternalEvent::TermEvent(event.clone()))
481 }
482 }
483 }
484
485 ///Takes events from Alacritty and translates them to behavior on this view
486 fn process_terminal_event(
487 &mut self,
488 event: &InternalEvent,
489 term: &mut Term<ZedListener>,
490 cx: &mut ModelContext<Self>,
491 ) {
492 match event {
493 InternalEvent::TermEvent(term_event) => {
494 if let AlacTermEvent::ColorRequest(index, format) = term_event {
495 let color = term.colors()[*index].unwrap_or_else(|| {
496 let term_style = &cx.global::<Settings>().theme.terminal;
497 to_alac_rgb(get_color_at_index(index, &term_style.colors))
498 });
499 self.write_to_pty(format(color))
500 }
501 }
502 InternalEvent::Resize(new_size) => {
503 self.cur_size = *new_size;
504
505 self.pty_tx.0.send(Msg::Resize((*new_size).into())).ok();
506
507 term.resize(*new_size);
508 }
509 InternalEvent::Clear => {
510 self.write_to_pty("\x0c".to_string());
511 term.clear_screen(ClearMode::Saved);
512 }
513 InternalEvent::Scroll(scroll) => term.scroll_display(*scroll),
514 InternalEvent::SetSelection(sel) => term.selection = sel.clone(),
515 InternalEvent::UpdateSelection((point, side)) => {
516 if let Some(mut selection) = term.selection.take() {
517 selection.update(*point, *side);
518 term.selection = Some(selection);
519 }
520 }
521
522 InternalEvent::Copy => {
523 if let Some(txt) = term.selection_to_string() {
524 cx.write_to_clipboard(ClipboardItem::new(txt))
525 }
526 }
527 }
528 }
529
530 pub fn input(&mut self, input: String) {
531 self.events.push(InternalEvent::Scroll(Scroll::Bottom));
532 self.events.push(InternalEvent::SetSelection(None));
533 self.write_to_pty(input);
534 }
535
536 ///Write the Input payload to the tty.
537 fn write_to_pty(&self, input: String) {
538 self.pty_tx.notify(input.into_bytes());
539 }
540
541 ///Resize the terminal and the PTY.
542 pub fn set_size(&mut self, new_size: TerminalSize) {
543 self.events.push(InternalEvent::Resize(new_size))
544 }
545
546 pub fn clear(&mut self) {
547 self.events.push(InternalEvent::Clear)
548 }
549
550 pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
551 let esc = to_esc_str(keystroke, &self.last_mode);
552 if let Some(esc) = esc {
553 self.input(esc);
554 true
555 } else {
556 false
557 }
558 }
559
560 ///Paste text into the terminal
561 pub fn paste(&mut self, text: &str) {
562 let paste_text = if self.last_mode.contains(TermMode::BRACKETED_PASTE) {
563 format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
564 } else {
565 text.replace("\r\n", "\r").replace('\n', "\r")
566 };
567 self.input(paste_text)
568 }
569
570 pub fn copy(&mut self) {
571 self.events.push(InternalEvent::Copy);
572 }
573
574 pub fn render_lock<F, T>(&mut self, cx: &mut ModelContext<Self>, f: F) -> T
575 where
576 F: FnOnce(RenderableContent, char) -> T,
577 {
578 let m = self.term.clone(); //Arc clone
579 let mut term = m.lock();
580
581 while let Some(e) = self.events.pop() {
582 self.process_terminal_event(&e, &mut term, cx)
583 }
584
585 self.last_mode = *term.mode();
586
587 let content = term.renderable_content();
588
589 self.last_offset = content.display_offset;
590
591 let cursor_text = term.grid()[content.cursor.point].c;
592
593 f(content, cursor_text)
594 }
595
596 pub fn focus_in(&self) {
597 if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
598 self.write_to_pty("\x1b[I".to_string());
599 }
600 }
601
602 pub fn focus_out(&self) {
603 if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
604 self.write_to_pty("\x1b[O".to_string());
605 }
606 }
607
608 pub fn mouse_changed(&mut self, point: Point, side: Direction) -> bool {
609 match self.last_mouse {
610 Some((old_point, old_side)) => {
611 if old_point == point && old_side == side {
612 false
613 } else {
614 self.last_mouse = Some((point, side));
615 true
616 }
617 }
618 None => {
619 self.last_mouse = Some((point, side));
620 true
621 }
622 }
623 }
624
625 pub fn mouse_mode(&self, shift: bool) -> bool {
626 self.last_mode.intersects(TermMode::MOUSE_MODE) && !shift
627 }
628
629 pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
630 let position = e.position.sub(origin);
631
632 let point = mouse_point(position, self.cur_size, self.last_offset);
633 let side = mouse_side(position, self.cur_size);
634
635 if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
636 if let Some(bytes) = mouse_moved_report(point, e, self.last_mode) {
637 self.pty_tx.notify(bytes);
638 }
639 }
640 }
641
642 pub fn mouse_drag(&mut self, e: MouseMovedEvent, origin: Vector2F) {
643 let position = e.position.sub(origin);
644
645 if !self.mouse_mode(e.shift) {
646 let point = mouse_point(position, self.cur_size, self.last_offset);
647 let side = mouse_side(position, self.cur_size);
648
649 self.events
650 .push(InternalEvent::UpdateSelection((point, side)));
651 }
652 }
653
654 pub fn mouse_down(&mut self, e: &MouseButtonEvent, origin: Vector2F) {
655 let position = e.position.sub(origin);
656 let point = mouse_point(position, self.cur_size, self.last_offset);
657 let side = mouse_side(position, self.cur_size);
658
659 if self.mouse_mode(e.shift) {
660 if let Some(bytes) = mouse_button_report(point, e, true, self.last_mode) {
661 self.pty_tx.notify(bytes);
662 }
663 } else if e.button == MouseButton::Left {
664 self.events
665 .push(InternalEvent::SetSelection(Some(Selection::new(
666 SelectionType::Simple,
667 point,
668 side,
669 ))));
670 }
671 }
672
673 pub fn left_click(&mut self, e: &MouseButtonEvent, origin: Vector2F) {
674 let position = e.position.sub(origin);
675
676 if !self.mouse_mode(e.shift) {
677 let point = mouse_point(position, self.cur_size, self.last_offset);
678 let side = mouse_side(position, self.cur_size);
679
680 let selection_type = match e.click_count {
681 0 => return, //This is a release
682 1 => Some(SelectionType::Simple),
683 2 => Some(SelectionType::Semantic),
684 3 => Some(SelectionType::Lines),
685 _ => None,
686 };
687
688 let selection =
689 selection_type.map(|selection_type| Selection::new(selection_type, point, side));
690
691 self.events.push(InternalEvent::SetSelection(selection));
692 }
693 }
694
695 pub fn mouse_up(&mut self, e: &MouseButtonEvent, origin: Vector2F) {
696 let position = e.position.sub(origin);
697 if self.mouse_mode(e.shift) {
698 let point = mouse_point(position, self.cur_size, self.last_offset);
699
700 if let Some(bytes) = mouse_button_report(point, e, false, self.last_mode) {
701 self.pty_tx.notify(bytes);
702 }
703 } else if e.button == MouseButton::Left {
704 // Seems pretty standard to automatically copy on mouse_up for terminals,
705 // so let's do that here
706 self.copy();
707 }
708 }
709
710 ///Scroll the terminal
711 pub fn scroll(&mut self, scroll: &ScrollWheelEvent, origin: Vector2F) {
712 if self.mouse_mode(scroll.shift) {
713 //TODO: Currently this only sends the current scroll reports as they come in. Alacritty
714 //Sends the *entire* scroll delta on *every* scroll event, only resetting it when
715 //The scroll enters 'TouchPhase::Started'. Do I need to replicate this?
716 //This would be consistent with a scroll model based on 'distance from origin'...
717 let scroll_lines = (scroll.delta.y() / self.cur_size.line_height) as i32;
718 let point = mouse_point(scroll.position.sub(origin), self.cur_size, self.last_offset);
719
720 if let Some(scrolls) = scroll_report(point, scroll_lines as i32, scroll, self.last_mode)
721 {
722 for scroll in scrolls {
723 self.pty_tx.notify(scroll);
724 }
725 };
726 } else if self
727 .last_mode
728 .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
729 && !scroll.shift
730 {
731 //TODO: See above TODO, also applies here.
732 let scroll_lines = ((scroll.delta.y() * ALACRITTY_SCROLL_MULTIPLIER)
733 / self.cur_size.line_height) as i32;
734
735 self.pty_tx.notify(alt_scroll(scroll_lines))
736 } else {
737 let scroll_lines = ((scroll.delta.y() * ALACRITTY_SCROLL_MULTIPLIER)
738 / self.cur_size.line_height) as i32;
739 if scroll_lines != 0 {
740 let scroll = Scroll::Delta(scroll_lines);
741 self.events.push(InternalEvent::Scroll(scroll));
742 }
743 }
744 }
745}
746
747impl Drop for Terminal {
748 fn drop(&mut self) {
749 self.pty_tx.0.send(Msg::Shutdown).ok();
750 }
751}
752
753impl Entity for Terminal {
754 type Event = Event;
755}
756
757#[cfg(test)]
758mod tests {
759 pub mod terminal_test_context;
760}
761
762//TODO Move this around and clean up the code
763mod alacritty_unix {
764 use alacritty_terminal::config::Program;
765 use gpui::anyhow::{bail, Result};
766
767 use std::ffi::CStr;
768 use std::mem::MaybeUninit;
769 use std::ptr;
770
771 #[derive(Debug)]
772 pub struct Passwd<'a> {
773 _name: &'a str,
774 _dir: &'a str,
775 pub shell: &'a str,
776 }
777
778 /// Return a Passwd struct with pointers into the provided buf.
779 ///
780 /// # Unsafety
781 ///
782 /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen.
783 pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result<Passwd<'_>> {
784 // Create zeroed passwd struct.
785 let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
786
787 let mut res: *mut libc::passwd = ptr::null_mut();
788
789 // Try and read the pw file.
790 let uid = unsafe { libc::getuid() };
791 let status = unsafe {
792 libc::getpwuid_r(
793 uid,
794 entry.as_mut_ptr(),
795 buf.as_mut_ptr() as *mut _,
796 buf.len(),
797 &mut res,
798 )
799 };
800 let entry = unsafe { entry.assume_init() };
801
802 if status < 0 {
803 bail!("getpwuid_r failed");
804 }
805
806 if res.is_null() {
807 bail!("pw not found");
808 }
809
810 // Sanity check.
811 assert_eq!(entry.pw_uid, uid);
812
813 // Build a borrowed Passwd struct.
814 Ok(Passwd {
815 _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() },
816 _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() },
817 shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() },
818 })
819 }
820
821 #[cfg(target_os = "macos")]
822 pub fn _default_shell(pw: &Passwd<'_>) -> Program {
823 let shell_name = pw.shell.rsplit('/').next().unwrap();
824 let argv = vec![
825 String::from("-c"),
826 format!("exec -a -{} {}", shell_name, pw.shell),
827 ];
828
829 Program::WithArgs {
830 program: "/bin/bash".to_owned(),
831 args: argv,
832 }
833 }
834
835 #[cfg(not(target_os = "macos"))]
836 pub fn default_shell(pw: &Passwd<'_>) -> Program {
837 Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned()))
838 }
839}