88//
99// TODO:
1010// - implement -f option (requires updates to utmpx module)
11- // - implement -T, -u options
1211//
1312
13+ use std:: os:: unix:: fs:: MetadataExt ;
1414use std:: path:: PathBuf ;
1515
1616use clap:: Parser ;
@@ -82,47 +82,130 @@ struct Args {
8282 file : Option < PathBuf > ,
8383}
8484
85- // convert timestamp into POSIX-specified strftime format
85+ // convert timestamp into POSIX-specified strftime format (local time)
8686fn fmt_timestamp ( ts : libc:: time_t ) -> String {
87- let dt = chrono:: DateTime :: from_timestamp ( ts, 0 ) . unwrap ( ) ;
87+ use chrono:: { Local , TimeZone } ;
88+ let dt = Local
89+ . timestamp_opt ( ts, 0 )
90+ . single ( )
91+ . unwrap_or_else ( Local :: now) ;
8892 dt. format ( "%b %e %H:%M" ) . to_string ( )
8993}
9094
91- fn print_fmt_short ( entry : & Utmpx , line : & str ) {
92- println ! (
93- "{:<16} {:<12} {}" ,
94- entry. user,
95- line,
96- fmt_timestamp( entry. timestamp)
97- ) ;
95+ // Get terminal state for -T option: + (write allowed), - (write denied), ? (unknown)
96+ fn get_terminal_state ( line : & str ) -> char {
97+ if line == "system boot" {
98+ return ' ' ;
99+ }
100+ let path = format ! ( "/dev/{}" , line) ;
101+ match std:: fs:: metadata ( & path) {
102+ Ok ( meta) => {
103+ let mode = meta. mode ( ) ;
104+ // Check group write permission (o+w would be 0o002, g+w is 0o020)
105+ if mode & 0o020 != 0 {
106+ '+'
107+ } else {
108+ '-'
109+ }
110+ }
111+ Err ( _) => '?' ,
112+ }
98113}
99114
100- fn print_fmt_term ( entry : & Utmpx , line : & str ) {
101- let term_state = '?' ;
102- println ! (
103- "{:<16} {} {:<12} {}" ,
104- entry. user,
105- term_state,
106- line,
107- fmt_timestamp( entry. timestamp)
108- ) ;
115+ // Get idle time for -u option
116+ fn get_idle_time ( line : & str ) -> String {
117+ if line == "system boot" {
118+ return " ." . to_string ( ) ;
119+ }
120+ let path = format ! ( "/dev/{}" , line) ;
121+ match std:: fs:: metadata ( & path) {
122+ Ok ( meta) => {
123+ let atime = meta. atime ( ) ;
124+ let now = std:: time:: SystemTime :: now ( )
125+ . duration_since ( std:: time:: UNIX_EPOCH )
126+ . unwrap ( )
127+ . as_secs ( ) as i64 ;
128+ let idle_secs = now - atime;
129+ if idle_secs < 60 {
130+ " ." . to_string ( ) // Active in last minute
131+ } else if idle_secs > 24 * 60 * 60 {
132+ " old" . to_string ( )
133+ } else {
134+ let hours = idle_secs / 3600 ;
135+ let mins = ( idle_secs % 3600 ) / 60 ;
136+ format ! ( "{:02}:{:02}" , hours, mins)
137+ }
138+ }
139+ Err ( _) => " ?" . to_string ( ) ,
140+ }
109141}
110142
111- fn current_terminal ( ) -> String {
112- let s = curuser:: tty ( ) ;
113- if let Some ( st) = s. strip_prefix ( "/dev/" ) {
114- st. to_owned ( )
143+ fn print_fmt_short ( args : & Args , entry : & Utmpx , line : & str ) {
144+ if args. idle_time {
145+ println ! (
146+ "{:<16} {:<12} {} {} {:>5}" ,
147+ entry. user,
148+ line,
149+ fmt_timestamp( entry. timestamp) ,
150+ get_idle_time( line) ,
151+ entry. pid
152+ ) ;
115153 } else {
116- s
154+ println ! (
155+ "{:<16} {:<12} {}" ,
156+ entry. user,
157+ line,
158+ fmt_timestamp( entry. timestamp)
159+ ) ;
117160 }
118161}
119162
163+ fn print_fmt_term ( args : & Args , entry : & Utmpx , line : & str ) {
164+ let term_state = get_terminal_state ( line) ;
165+ if args. idle_time {
166+ println ! (
167+ "{:<16} {} {:<12} {} {} {:>5}" ,
168+ entry. user,
169+ term_state,
170+ line,
171+ fmt_timestamp( entry. timestamp) ,
172+ get_idle_time( line) ,
173+ entry. pid
174+ ) ;
175+ } else {
176+ println ! (
177+ "{:<16} {} {:<12} {}" ,
178+ entry. user,
179+ term_state,
180+ line,
181+ fmt_timestamp( entry. timestamp)
182+ ) ;
183+ }
184+ }
185+
186+ fn current_terminal ( ) -> Option < String > {
187+ curuser:: tty ( ) . map ( |s| {
188+ if let Some ( st) = s. strip_prefix ( "/dev/" ) {
189+ st. to_owned ( )
190+ } else {
191+ s
192+ }
193+ } )
194+ }
195+
120196fn print_entry ( args : & Args , entry : & Utmpx ) {
121197 // Skip if current_terminal option is set and this entry is not for the current terminal
122198 if args. current_terminal {
123- let current_tty = current_terminal ( ) ;
124- if entry. line != current_tty {
125- return ;
199+ match current_terminal ( ) {
200+ Some ( current_tty) => {
201+ if entry. line != current_tty {
202+ return ;
203+ }
204+ }
205+ None => {
206+ // No tty available, skip all entries for -m
207+ return ;
208+ }
126209 }
127210 }
128211
@@ -146,18 +229,18 @@ fn print_entry(args: &Args, entry: &Utmpx) {
146229 _ => entry. line . as_str ( ) ,
147230 } ;
148231
149- if args. short_format {
150- print_fmt_short ( entry, line) ;
232+ if args. terminals {
233+ print_fmt_term ( args , entry, line) ;
151234 } else {
152- print_fmt_term ( entry, line) ;
235+ print_fmt_short ( args , entry, line) ;
153236 }
154237}
155238
156239fn show_utmpx_entries ( args : & Args ) {
157240 if args. heading {
158241 println ! (
159242 "{:<16} {:<12} {}" ,
160- gettext( "USER " ) ,
243+ gettext( "NAME " ) ,
161244 gettext( "LINE" ) ,
162245 gettext( "TIME" )
163246 ) ;
@@ -170,16 +253,16 @@ fn show_utmpx_entries(args: &Args) {
170253}
171254
172255fn show_utmpx_summary ( ) {
173- let mut count = 0 ;
174256 let entries = utmpx:: load ( ) ;
175- for entry in & entries {
176- if !entry. user . is_empty ( ) {
177- println ! ( "{}" , entry. user) ;
178- count += 1 ;
179- }
180- }
181-
182- println ! ( "# users = {}" , count) ;
257+ let users: Vec < & str > = entries
258+ . iter ( )
259+ . filter ( |e| !e. user . is_empty ( ) && e. typ == platform:: USER_PROCESS )
260+ . map ( |e| e. user . as_str ( ) )
261+ . collect ( ) ;
262+
263+ // Print users horizontally, space-separated (POSIX format)
264+ println ! ( "{}" , users. join( " " ) ) ;
265+ println ! ( "# users={}" , users. len( ) ) ;
183266}
184267
185268fn main ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
0 commit comments