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