1
1
use std:: {
2
2
cell:: RefCell ,
3
3
env, fs,
4
- io:: { self , Write } ,
4
+ io:: { self , BufRead , BufReader , Read , Write } ,
5
5
mem,
6
6
path:: PathBuf ,
7
7
rc:: Rc ,
8
8
sync:: Once ,
9
- time:: Duration ,
9
+ time:: { Duration , SystemTime , UNIX_EPOCH } ,
10
10
} ;
11
11
12
12
use abi_stable:: std_types:: { ROption , RVec } ;
13
13
use anyrun_interface:: { HandleResult , Match , PluginInfo , PluginRef , PollResult } ;
14
14
use clap:: { Parser , ValueEnum } ;
15
15
use gtk:: { gdk, gdk_pixbuf, gio, glib, prelude:: * } ;
16
16
use nix:: unistd;
17
- use serde:: Deserialize ;
17
+ use serde:: { Deserialize , Serialize } ;
18
18
use wl_clipboard_rs:: copy;
19
19
20
20
#[ anyrun_macros:: config_args]
@@ -49,6 +49,10 @@ struct Config {
49
49
max_entries : Option < usize > ,
50
50
#[ serde( default = "Config::default_layer" ) ]
51
51
layer : Layer ,
52
+ #[ serde( default ) ]
53
+ persist_state : bool ,
54
+ #[ serde( default ) ]
55
+ state_ttl_secs : Option < u64 > ,
52
56
}
53
57
54
58
impl Config {
@@ -97,6 +101,8 @@ impl Default for Config {
97
101
show_results_immediately : false ,
98
102
max_entries : None ,
99
103
layer : Self :: default_layer ( ) ,
104
+ persist_state : false ,
105
+ state_ttl_secs : None ,
100
106
}
101
107
}
102
108
}
@@ -178,6 +184,67 @@ struct RuntimeData {
178
184
config_dir : String ,
179
185
}
180
186
187
+ impl RuntimeData {
188
+ fn state_file ( & self ) -> String {
189
+ format ! ( "{}/state.txt" , self . config_dir)
190
+ }
191
+
192
+ fn save_state ( & self , text : & str ) -> io:: Result < ( ) > {
193
+ if !self . config . persist_state {
194
+ return Ok ( ( ) ) ;
195
+ }
196
+ let timestamp = SystemTime :: now ( )
197
+ . duration_since ( UNIX_EPOCH )
198
+ . unwrap ( )
199
+ . as_millis ( ) ;
200
+
201
+ let mut file = fs:: File :: create ( self . state_file ( ) ) ?;
202
+ writeln ! ( file, "{}" , timestamp) ?;
203
+ write ! ( file, "{}" , text)
204
+ }
205
+
206
+ fn load_state ( & self ) -> io:: Result < String > {
207
+ if !self . config . persist_state {
208
+ return Ok ( String :: new ( ) ) ;
209
+ }
210
+ match fs:: File :: open ( self . state_file ( ) ) {
211
+ Ok ( file) => {
212
+ let mut reader = BufReader :: new ( file) ;
213
+
214
+ // Read timestamp from first line
215
+ let mut timestamp_str = String :: new ( ) ;
216
+ reader. read_line ( & mut timestamp_str) ?;
217
+ let timestamp = timestamp_str. trim ( ) . parse :: < u128 > ( ) . unwrap_or ( 0 ) ;
218
+
219
+ // Check if state has expired
220
+ if let Some ( expiry_secs) = self . config . state_ttl_secs {
221
+ let now = SystemTime :: now ( )
222
+ . duration_since ( UNIX_EPOCH )
223
+ . unwrap ( )
224
+ . as_millis ( ) ;
225
+ if now - timestamp > u128:: from ( expiry_secs) * 1000 {
226
+ return Ok ( String :: new ( ) ) ;
227
+ }
228
+ }
229
+
230
+ // Read text from second line to end
231
+ let mut text = String :: new ( ) ;
232
+ reader. read_to_string ( & mut text) ?;
233
+ Ok ( text)
234
+ }
235
+ Err ( e) if e. kind ( ) == io:: ErrorKind :: NotFound => Ok ( String :: new ( ) ) ,
236
+ Err ( e) => Err ( e) ,
237
+ }
238
+ }
239
+
240
+ fn clear_state ( & self ) -> io:: Result < ( ) > {
241
+ if !self . config . persist_state {
242
+ return Ok ( ( ) ) ;
243
+ }
244
+ fs:: write ( self . state_file ( ) , "0\n " )
245
+ }
246
+ }
247
+
181
248
/// The naming scheme for CSS styling
182
249
///
183
250
/// Refer to [GTK 3.0 CSS Overview](https://docs.gtk.org/gtk3/css-overview.html)
@@ -251,7 +318,7 @@ fn main() {
251
318
252
319
config. merge_opt ( args. config ) ;
253
320
254
- let runtime_data: Rc < RefCell < RuntimeData > > = Rc :: new ( RefCell :: new ( RuntimeData {
321
+ let runtime_data = Rc :: new ( RefCell :: new ( RuntimeData {
255
322
exclusive : None ,
256
323
plugins : Vec :: new ( ) ,
257
324
post_run_action : PostRunAction :: None ,
@@ -465,10 +532,21 @@ fn activate(app: >k::Application, runtime_data: Rc<RefCell<RuntimeData>>) {
465
532
. name ( style_names:: ENTRY )
466
533
. build ( ) ;
467
534
468
- // Refresh the matches when text input changes
535
+ // Set initial text from loaded state
536
+ if let Ok ( initial_text) = runtime_data. borrow ( ) . load_state ( ) {
537
+ entry. set_text ( & initial_text) ;
538
+ } else {
539
+ eprintln ! ( "Failed to load state" ) ;
540
+ }
541
+
542
+ // Update last_input, save state and refresh matches when text changes
469
543
let runtime_data_clone = runtime_data. clone ( ) ;
470
544
entry. connect_changed ( move |entry| {
471
- refresh_matches ( entry. text ( ) . to_string ( ) , runtime_data_clone. clone ( ) )
545
+ let text = entry. text ( ) . to_string ( ) ;
546
+ if let Err ( e) = runtime_data_clone. borrow ( ) . save_state ( & text) {
547
+ eprintln ! ( "Failed to save state: {}" , e) ;
548
+ }
549
+ refresh_matches ( text, runtime_data_clone. clone ( ) ) ;
472
550
} ) ;
473
551
474
552
// Handle other key presses for selection control and all other things that may be needed
@@ -584,6 +662,9 @@ fn activate(app: >k::Application, runtime_data: Rc<RefCell<RuntimeData>>) {
584
662
( * selected_match. data :: < Match > ( "match" ) . unwrap ( ) . as_ptr ( ) ) . clone ( )
585
663
} ) {
586
664
HandleResult :: Close => {
665
+ if let Err ( e) = _runtime_data_clone. clear_state ( ) {
666
+ eprintln ! ( "Failed to clear state: {}" , e) ;
667
+ }
587
668
window. close ( ) ;
588
669
Inhibit ( true )
589
670
}
@@ -599,13 +680,19 @@ fn activate(app: >k::Application, runtime_data: Rc<RefCell<RuntimeData>>) {
599
680
}
600
681
HandleResult :: Copy ( bytes) => {
601
682
_runtime_data_clone. post_run_action = PostRunAction :: Copy ( bytes. into ( ) ) ;
683
+ if let Err ( e) = _runtime_data_clone. clear_state ( ) {
684
+ eprintln ! ( "Failed to clear state: {}" , e) ;
685
+ }
602
686
window. close ( ) ;
603
687
Inhibit ( true )
604
688
}
605
689
HandleResult :: Stdout ( bytes) => {
606
690
if let Err ( why) = io:: stdout ( ) . lock ( ) . write_all ( & bytes) {
607
691
eprintln ! ( "Error outputting content to stdout: {}" , why) ;
608
692
}
693
+ if let Err ( e) = _runtime_data_clone. clear_state ( ) {
694
+ eprintln ! ( "Failed to clear state: {}" , e) ;
695
+ }
609
696
window. close ( ) ;
610
697
Inhibit ( true )
611
698
}
@@ -679,11 +766,17 @@ fn activate(app: >k::Application, runtime_data: Rc<RefCell<RuntimeData>>) {
679
766
main_vbox. add ( & main_list) ;
680
767
main_list. show ( ) ;
681
768
entry. grab_focus ( ) ; // Grab the focus so typing is immediately accepted by the entry box
769
+ entry. set_position ( -1 ) ; // -1 moves cursor to end of text in case some text was restored
682
770
}
683
771
684
- if runtime_data. borrow ( ) . config . show_results_immediately {
685
- // Get initial matches
686
- refresh_matches ( String :: new ( ) , runtime_data) ;
772
+ // Show initial results if state restoration is enabled or immediate results are configured
773
+ let should_show_results = {
774
+ let data = runtime_data. borrow ( ) ;
775
+ data. config . persist_state || data. config . show_results_immediately
776
+ } ;
777
+
778
+ if should_show_results {
779
+ refresh_matches ( entry. text ( ) . to_string ( ) , runtime_data) ;
687
780
}
688
781
} ) ;
689
782
0 commit comments