11use std:: {
22 cell:: RefCell ,
33 env, fs,
4- io:: { self , Write } ,
4+ io:: { self , BufRead , BufReader , Read , Write } ,
55 mem,
66 path:: PathBuf ,
77 rc:: Rc ,
88 sync:: Once ,
9- time:: Duration ,
9+ time:: { Duration , SystemTime , UNIX_EPOCH } ,
1010} ;
1111
1212use abi_stable:: std_types:: { ROption , RVec } ;
1313use anyrun_interface:: { HandleResult , Match , PluginInfo , PluginRef , PollResult } ;
1414use clap:: { Parser , ValueEnum } ;
1515use gtk:: { gdk, gdk_pixbuf, gio, glib, prelude:: * } ;
1616use nix:: unistd;
17- use serde:: Deserialize ;
17+ use serde:: { Deserialize , Serialize } ;
1818use wl_clipboard_rs:: copy;
1919
2020#[ anyrun_macros:: config_args]
@@ -49,6 +49,10 @@ struct Config {
4949 max_entries : Option < usize > ,
5050 #[ serde( default = "Config::default_layer" ) ]
5151 layer : Layer ,
52+ #[ serde( default ) ]
53+ persist_state : bool ,
54+ #[ serde( default ) ]
55+ state_ttl_secs : Option < u64 > ,
5256}
5357
5458impl Config {
@@ -97,6 +101,8 @@ impl Default for Config {
97101 show_results_immediately : false ,
98102 max_entries : None ,
99103 layer : Self :: default_layer ( ) ,
104+ persist_state : false ,
105+ state_ttl_secs : None ,
100106 }
101107 }
102108}
@@ -178,6 +184,67 @@ struct RuntimeData {
178184 config_dir : String ,
179185}
180186
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+
181248/// The naming scheme for CSS styling
182249///
183250/// Refer to [GTK 3.0 CSS Overview](https://docs.gtk.org/gtk3/css-overview.html)
@@ -251,7 +318,7 @@ fn main() {
251318
252319 config. merge_opt ( args. config ) ;
253320
254- let runtime_data: Rc < RefCell < RuntimeData > > = Rc :: new ( RefCell :: new ( RuntimeData {
321+ let runtime_data = Rc :: new ( RefCell :: new ( RuntimeData {
255322 exclusive : None ,
256323 plugins : Vec :: new ( ) ,
257324 post_run_action : PostRunAction :: None ,
@@ -465,10 +532,21 @@ fn activate(app: >k::Application, runtime_data: Rc<RefCell<RuntimeData>>) {
465532 . name ( style_names:: ENTRY )
466533 . build ( ) ;
467534
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
469543 let runtime_data_clone = runtime_data. clone ( ) ;
470544 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 ( ) ) ;
472550 } ) ;
473551
474552 // 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>>) {
584662 ( * selected_match. data :: < Match > ( "match" ) . unwrap ( ) . as_ptr ( ) ) . clone ( )
585663 } ) {
586664 HandleResult :: Close => {
665+ if let Err ( e) = _runtime_data_clone. clear_state ( ) {
666+ eprintln ! ( "Failed to clear state: {}" , e) ;
667+ }
587668 window. close ( ) ;
588669 Inhibit ( true )
589670 }
@@ -599,13 +680,19 @@ fn activate(app: >k::Application, runtime_data: Rc<RefCell<RuntimeData>>) {
599680 }
600681 HandleResult :: Copy ( bytes) => {
601682 _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+ }
602686 window. close ( ) ;
603687 Inhibit ( true )
604688 }
605689 HandleResult :: Stdout ( bytes) => {
606690 if let Err ( why) = io:: stdout ( ) . lock ( ) . write_all ( & bytes) {
607691 eprintln ! ( "Error outputting content to stdout: {}" , why) ;
608692 }
693+ if let Err ( e) = _runtime_data_clone. clear_state ( ) {
694+ eprintln ! ( "Failed to clear state: {}" , e) ;
695+ }
609696 window. close ( ) ;
610697 Inhibit ( true )
611698 }
@@ -679,11 +766,17 @@ fn activate(app: >k::Application, runtime_data: Rc<RefCell<RuntimeData>>) {
679766 main_vbox. add ( & main_list) ;
680767 main_list. show ( ) ;
681768 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
682770 }
683771
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) ;
687780 }
688781 } ) ;
689782
0 commit comments