diff --git a/.c2pconfig b/.c2pconfig new file mode 100644 index 0000000..b60e458 --- /dev/null +++ b/.c2pconfig @@ -0,0 +1,8 @@ +default_output = "clipboard" +include_patterns = ["*.rs"] +exclude_patterns = ["**/test*"] +line_numbers = false +absolute_path = true + +[user_variables] +project = "code2prompt" diff --git a/Cargo.lock b/Cargo.lock index 7d367ec..617e502 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -52,9 +52,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -67,9 +67,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -168,9 +168,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -178,7 +178,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -270,9 +270,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder-lite" @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.38" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ "find-msvc-tools", "jobserver", @@ -339,7 +339,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -404,7 +404,7 @@ dependencies = [ [[package]] name = "code2prompt" -version = "4.0.3" +version = "4.1.0" dependencies = [ "ansi_term", "anyhow", @@ -436,6 +436,7 @@ dependencies = [ "tempfile", "terminal_size", "tokio", + "toml", "tui-textarea", "tui-tree-widget", "unicode-width 0.2.0", @@ -454,7 +455,7 @@ dependencies = [ [[package]] name = "code2prompt_core" -version = "4.0.2" +version = "4.1.0" dependencies = [ "anyhow", "assert_cmd", @@ -479,6 +480,7 @@ dependencies = [ "tempfile", "termtree", "tiktoken-rs", + "toml", ] [[package]] @@ -520,7 +522,7 @@ dependencies = [ "libc", "once_cell", "unicode-width 0.2.0", - "windows-sys 0.61.0", + "windows-sys 0.61.1", ] [[package]] @@ -792,7 +794,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.0", + "windows-sys 0.61.1", ] [[package]] @@ -900,7 +902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.61.1", ] [[package]] @@ -957,9 +959,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" [[package]] name = "fixedbitset" @@ -1156,9 +1158,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "git2" @@ -1552,9 +1554,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -1584,9 +1586,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libgit2-sys" @@ -1608,7 +1610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -1659,11 +1661,10 @@ checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -1695,9 +1696,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -1823,9 +1824,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] @@ -1844,9 +1845,9 @@ dependencies = [ [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.4", "dispatch2", @@ -1855,9 +1856,9 @@ dependencies = [ [[package]] name = "objc2-core-graphics" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.9.4", "dispatch2", @@ -1874,9 +1875,9 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.9.4", "objc2", @@ -1885,9 +1886,9 @@ dependencies = [ [[package]] name = "objc2-io-surface" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.9.4", "objc2", @@ -1896,9 +1897,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -1923,9 +1924,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.2+3.5.2" +version = "300.5.3+3.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" +checksum = "dc6bad8cd0233b63971e232cc9c5e83039375b8586d2312f31fda85db8f888c2" dependencies = [ "cc", ] @@ -1961,9 +1962,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1971,15 +1972,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -2002,20 +2003,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -2023,9 +2023,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", @@ -2036,9 +2036,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", "sha2", @@ -2244,9 +2244,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -2300,9 +2300,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.9.4", ] @@ -2320,9 +2320,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.2" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", @@ -2332,9 +2332,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", @@ -2455,7 +2455,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.0", + "windows-sys 0.61.1", ] [[package]] @@ -2493,9 +2493,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -2503,18 +2503,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2534,6 +2534,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +dependencies = [ + "serde_core", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2698,7 +2707,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.1.2", - "windows-sys 0.61.0", + "windows-sys 0.61.1", ] [[package]] @@ -2719,18 +2728,18 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -2816,6 +2825,21 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "toml" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.7.2" @@ -2846,6 +2870,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" + [[package]] name = "tree_magic_mini" version = "3.2.0" @@ -2881,9 +2911,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -3019,9 +3049,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", @@ -3032,9 +3062,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", @@ -3046,9 +3076,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3056,9 +3086,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -3069,9 +3099,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -3202,7 +3232,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.1", ] [[package]] @@ -3213,22 +3243,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.62.0" +version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", + "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" dependencies = [ "proc-macro2", "quote", @@ -3237,21 +3267,15 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.0" @@ -3264,7 +3288,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -3273,7 +3297,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -3300,16 +3324,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.4", ] [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -3330,11 +3354,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" dependencies = [ - "windows-link 0.1.3", + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/Cargo.toml b/Cargo.toml index 5c5d05b..0d218e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ log = "0.4" num-format = { version = "0.4.4", features = ["with-system-locale"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.114" +toml = "0.9.7" handlebars = "6.3.2" jwalk = "0.8" termtree = "0.5" diff --git a/crates/code2prompt-core/Cargo.toml b/crates/code2prompt-core/Cargo.toml index 9f6980c..b0ec28a 100644 --- a/crates/code2prompt-core/Cargo.toml +++ b/crates/code2prompt-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "code2prompt_core" -version = "4.0.2" +version = "4.1.0" authors = [ "Mufeed VH ", "Olivier D'Ancona ", @@ -28,6 +28,7 @@ log = { workspace = true } num-format = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +toml = { workspace = true } handlebars = { workspace = true } jwalk = { workspace = true } termtree = { workspace = true } diff --git a/crates/code2prompt-core/src/configuration.rs b/crates/code2prompt-core/src/configuration.rs index 909d862..f0fb5e4 100644 --- a/crates/code2prompt-core/src/configuration.rs +++ b/crates/code2prompt-core/src/configuration.rs @@ -6,6 +6,7 @@ use crate::template::OutputFormat; use crate::tokenizer::TokenizerType; use crate::{sort::FileSortMethod, tokenizer::TokenFormat}; use derive_builder::Builder; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; @@ -108,3 +109,197 @@ impl Code2PromptConfig { Code2PromptConfigBuilder::default() } } + +/// Output destination for code2prompt +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum OutputDestination { + #[default] + Stdout, + Clipboard, + File, +} + +/// TOML configuration structure that can be serialized/deserialized +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct TomlConfig { + /// Default output behavior: "stdout", "clipboard", or "file" + #[serde(default)] + pub default_output: OutputDestination, + + /// Path to the codebase directory + #[serde(default)] + pub path: Option, + + /// Patterns to include + #[serde(default)] + pub include_patterns: Vec, + + /// Patterns to exclude + #[serde(default)] + pub exclude_patterns: Vec, + + /// Display options + #[serde(default)] + pub line_numbers: bool, + #[serde(default)] + pub absolute_path: bool, + #[serde(default)] + pub full_directory_tree: bool, + + /// Output format + #[serde(default)] + pub output_format: Option, + + /// Sort method + #[serde(default)] + pub sort_method: Option, + + /// Tokenizer settings + #[serde(default)] + pub encoding: Option, + #[serde(default)] + pub token_format: Option, + + /// Git settings + #[serde(default)] + pub diff_enabled: bool, + #[serde(default)] + pub diff_branches: Option>, + #[serde(default)] + pub log_branches: Option>, + + /// Template settings + #[serde(default)] + pub template_name: Option, + #[serde(default)] + pub template_str: Option, + + /// User variables + #[serde(default)] + pub user_variables: HashMap, + + /// Token map + #[serde(default)] + pub token_map_enabled: bool, +} + +impl TomlConfig { + /// Load TOML configuration from a string + pub fn from_toml_str(content: &str) -> Result { + toml::from_str(content) + } + + /// Convert TOML configuration to string + pub fn to_string(&self) -> Result { + toml::to_string_pretty(self) + } + + /// Convert TomlConfig to Code2PromptConfig + pub fn to_code2prompt_config(&self) -> Code2PromptConfig { + let mut builder = Code2PromptConfig::builder(); + + if let Some(path) = &self.path { + builder.path(PathBuf::from(path)); + } + + builder + .include_patterns(self.include_patterns.clone()) + .exclude_patterns(self.exclude_patterns.clone()) + .line_numbers(self.line_numbers) + .absolute_path(self.absolute_path) + .full_directory_tree(self.full_directory_tree); + + if let Some(output_format) = &self.output_format + && let Ok(format) = output_format.parse::() + { + builder.output_format(format); + } + + if let Some(sort_method) = &self.sort_method + && let Ok(method) = sort_method.parse::() + { + builder.sort_method(Some(method)); + } + + if let Some(encoding) = &self.encoding + && let Ok(tokenizer) = encoding.parse::() + { + builder.encoding(tokenizer); + } + + if let Some(token_format) = &self.token_format + && let Ok(format) = token_format.parse::() + { + builder.token_format(format); + } + + builder.diff_enabled(self.diff_enabled); + + if let Some(diff_branches) = &self.diff_branches + && diff_branches.len() == 2 + { + builder.diff_branches(Some((diff_branches[0].clone(), diff_branches[1].clone()))); + } + + if let Some(log_branches) = &self.log_branches + && log_branches.len() == 2 + { + builder.log_branches(Some((log_branches[0].clone(), log_branches[1].clone()))); + } + + if let Some(template_name) = &self.template_name { + builder.template_name(template_name.clone()); + } + + if let Some(template_str) = &self.template_str { + builder.template_str(template_str.clone()); + } + + builder + .user_variables(self.user_variables.clone()) + .token_map_enabled(self.token_map_enabled); + + builder.build().unwrap_or_default() + } +} + +/// Export a Code2PromptConfig to TOML format +pub fn export_config_to_toml(config: &Code2PromptConfig) -> Result { + let toml_config = TomlConfig { + default_output: OutputDestination::Stdout, // Default for new behavior + path: Some(config.path.to_string_lossy().to_string()), + include_patterns: config.include_patterns.clone(), + exclude_patterns: config.exclude_patterns.clone(), + line_numbers: config.line_numbers, + absolute_path: config.absolute_path, + full_directory_tree: config.full_directory_tree, + output_format: Some(config.output_format.to_string()), + sort_method: config.sort_method.as_ref().map(|s| s.to_string()), + encoding: Some(config.encoding.to_string()), + token_format: Some(config.token_format.to_string()), + diff_enabled: config.diff_enabled, + diff_branches: config + .diff_branches + .as_ref() + .map(|(a, b)| vec![a.clone(), b.clone()]), + log_branches: config + .log_branches + .as_ref() + .map(|(a, b)| vec![a.clone(), b.clone()]), + template_name: if config.template_name.is_empty() { + None + } else { + Some(config.template_name.clone()) + }, + template_str: if config.template_str.is_empty() { + None + } else { + Some(config.template_str.clone()) + }, + user_variables: config.user_variables.clone(), + token_map_enabled: config.token_map_enabled, + }; + + toml_config.to_string() +} diff --git a/crates/code2prompt-core/src/template.rs b/crates/code2prompt-core/src/template.rs index eca3386..c34d41d 100644 --- a/crates/code2prompt-core/src/template.rs +++ b/crates/code2prompt-core/src/template.rs @@ -1,7 +1,7 @@ //! This module contains the functions to set up the Handlebars template engine and render the template with the provided data. //! It also includes functions for handling user-defined variables, copying the rendered output to the clipboard, and writing it to a file. -use anyhow::{anyhow, Result}; -use handlebars::{no_escape, Handlebars}; +use anyhow::{Result, anyhow}; +use handlebars::{Handlebars, no_escape}; use regex::Regex; use std::io::Write; use std::str::FromStr; @@ -93,6 +93,16 @@ pub enum OutputFormat { Xml, } +impl std::fmt::Display for OutputFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OutputFormat::Markdown => write!(f, "markdown"), + OutputFormat::Json => write!(f, "json"), + OutputFormat::Xml => write!(f, "xml"), + } + } +} + impl FromStr for OutputFormat { type Err = anyhow::Error; diff --git a/crates/code2prompt/Cargo.toml b/crates/code2prompt/Cargo.toml index da1c67f..d671f0c 100644 --- a/crates/code2prompt/Cargo.toml +++ b/crates/code2prompt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "code2prompt" -version = "4.0.3" +version = "4.1.0" edition = "2024" description = "Command-line interface for code2prompt" license = "MIT" @@ -10,7 +10,7 @@ repository = "https://github.com/mufeedvh/code2prompt" wayland = ["arboard/wayland-data-control"] [dependencies] -code2prompt_core = { path = "../code2prompt-core", version = "4.0.2" } +code2prompt_core = { path = "../code2prompt-core", version = "4.1.0" } clap = { workspace = true } env_logger = { workspace = true } arboard = { workspace = true } @@ -21,6 +21,7 @@ log = { workspace = true } num-format = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +toml = { workspace = true } inquire = { workspace = true } terminal_size = { workspace = true } lscolors = { workspace = true } diff --git a/crates/code2prompt/src/args.rs b/crates/code2prompt/src/args.rs index 0dbb3d7..62c594b 100644 --- a/crates/code2prompt/src/args.rs +++ b/crates/code2prompt/src/args.rs @@ -60,7 +60,6 @@ pub struct Cli { /// Tokenizer to use for token count #[clap( - short = 'c', long, value_name = "cl100k, p50k, p50k_edit, r50k, gpt2", default_value = "cl100k" @@ -103,8 +102,12 @@ pub struct Cli { #[clap(long)] pub no_codeblock: bool, - /// Optional Disable copying to clipboard - #[clap(long)] + /// Copy output to clipboard + #[clap(short = 'c', long)] + pub clipboard: bool, + + /// Optional Disable copying to clipboard (deprecated, use default behavior) + #[clap(long, hide = true)] pub no_clipboard: bool, /// Skip .gitignore rules diff --git a/crates/code2prompt/src/config.rs b/crates/code2prompt/src/config.rs index 33dd0e6..37af8d8 100644 --- a/crates/code2prompt/src/config.rs +++ b/crates/code2prompt/src/config.rs @@ -13,7 +13,156 @@ use inquire::Text; use log::error; use std::{path::PathBuf, str::FromStr}; -use crate::args::Cli; +use crate::{args::Cli, config_loader::ConfigSource}; + +/// Create a Code2PromptSession from config and command line arguments +/// +/// # Arguments +/// +/// * `config_source` - The loaded configuration source +/// * `args` - The parsed command line arguments +/// * `tui_mode` - Whether the application is running in TUI mode +/// +/// # Returns +/// +/// * `Result` - The configured session or an error +pub fn create_session_from_config_and_args( + config_source: &ConfigSource, + args: &Cli, + tui_mode: bool, +) -> Result { + let mut configuration = Code2PromptConfig::builder(); + + // Start with config file values + let config = &config_source.config; + + // Apply config file settings first + if let Some(path) = &config.path { + configuration.path(PathBuf::from(path)); + } else { + configuration.path(args.path.clone()); + } + + // CLI args override config for patterns + // If CLI provides any patterns, they completely override config patterns to avoid conflicts + let (include_patterns, exclude_patterns) = + if !args.include.is_empty() || !args.exclude.is_empty() { + // CLI patterns provided - use only CLI patterns + ( + expand_comma_separated_patterns(&args.include), + expand_comma_separated_patterns(&args.exclude), + ) + } else { + // No CLI patterns - use config patterns + ( + config.include_patterns.clone(), + config.exclude_patterns.clone(), + ) + }; + + configuration + .include_patterns(include_patterns) + .exclude_patterns(exclude_patterns); + + // CLI args override config for display options + configuration + .line_numbers(args.line_numbers || config.line_numbers) + .absolute_path(args.absolute_paths || config.absolute_path) + .full_directory_tree(args.full_directory_tree || config.full_directory_tree); + + // Output format: CLI overrides config + let output_format = args.output_format.clone(); + configuration.output_format(output_format); + + // Sort method: CLI overrides config + let sort_method = if let Some(sort_str) = &args.sort { + sort_str.parse::().unwrap_or_else(|err| { + eprintln!("{}", err); + std::process::exit(1); + }) + } else if let Some(sort_str) = &config.sort_method { + sort_str + .parse::() + .unwrap_or(FileSortMethod::NameAsc) + } else { + FileSortMethod::NameAsc + }; + + configuration.sort_method(sort_method); + + // Tokenizer: CLI overrides config + let tokenizer_type = if let Some(encoding) = &args.encoding { + encoding.parse::().unwrap_or_default() + } else if let Some(encoding) = &config.encoding { + encoding.parse::().unwrap_or_default() + } else { + "cl100k".parse::().unwrap_or_default() + }; + + configuration + .encoding(tokenizer_type) + .token_format(args.tokens.clone()); + + // Template: CLI overrides config + let (template_str, template_name) = if args.template.is_some() { + parse_template(&args.template).unwrap_or_else(|e| { + error!("Failed to parse template: {}", e); + std::process::exit(1); + }) + } else { + ( + config.template_str.clone().unwrap_or_default(), + config + .template_name + .clone() + .unwrap_or_else(|| "default".to_string()), + ) + }; + + configuration + .template_str(template_str) + .template_name(template_name); + + // Git options: CLI overrides config + let diff_branches = parse_branch_argument(&args.git_diff_branch).or_else(|| { + config.diff_branches.as_ref().and_then(|branches| { + if branches.len() == 2 { + Some((branches[0].clone(), branches[1].clone())) + } else { + None + } + }) + }); + + let log_branches = parse_branch_argument(&args.git_log_branch).or_else(|| { + config.log_branches.as_ref().and_then(|branches| { + if branches.len() == 2 { + Some((branches[0].clone(), branches[1].clone())) + } else { + None + } + }) + }); + + configuration + .diff_enabled(args.diff || config.diff_enabled) + .diff_branches(diff_branches) + .log_branches(log_branches); + + // Other CLI flags + configuration + .no_ignore(args.no_ignore) + .hidden(args.hidden) + .no_codeblock(args.no_codeblock) + .follow_symlinks(args.follow_symlinks) + .token_map_enabled(args.token_map || config.token_map_enabled || tui_mode); + + // User variables from config + configuration.user_variables(config.user_variables.clone()); + + let session = Code2PromptSession::new(configuration.build()?); + Ok(session) +} /// Create a Code2PromptSession from command line arguments /// diff --git a/crates/code2prompt/src/config_loader.rs b/crates/code2prompt/src/config_loader.rs new file mode 100644 index 0000000..f1b433d --- /dev/null +++ b/crates/code2prompt/src/config_loader.rs @@ -0,0 +1,99 @@ +//! Configuration file loading and management. +//! +//! This module handles loading TOML configuration files from multiple locations +//! with proper priority handling and informational messages. + +use anyhow::{Context, Result}; +use code2prompt_core::configuration::{OutputDestination, TomlConfig}; +use colored::*; +use log::{debug, info}; +use std::path::PathBuf; + +/// Configuration source information +#[derive(Debug, Clone)] +pub struct ConfigSource { + pub config: TomlConfig, +} + +/// Load configuration with proper priority handling +pub fn load_config(quiet: bool) -> Result { + // Check for local config first (.c2pconfig in current directory) + let local_config_path = std::env::current_dir()?.join(".c2pconfig"); + if local_config_path.exists() { + match load_config_from_file(&local_config_path) { + Ok(config) => { + if !quiet { + eprintln!( + "{}{}{} Using config from: {}", + "[".bold().white(), + "i".bold().blue(), + "]".bold().white(), + local_config_path.display() + ); + } + info!("Loaded local config from: {}", local_config_path.display()); + return Ok(ConfigSource { config }); + } + Err(e) => { + debug!("Failed to load local config: {}", e); + } + } + } + + // Check for global config (~/.config/code2prompt/.c2pconfig) + if let Some(config_dir) = dirs::config_dir() { + let global_config_path = config_dir.join("code2prompt").join(".c2pconfig"); + if global_config_path.exists() { + match load_config_from_file(&global_config_path) { + Ok(config) => { + if !quiet { + eprintln!( + "{}{}{} Using config from: {}", + "[".bold().white(), + "i".bold().blue(), + "]".bold().white(), + global_config_path.display() + ); + } + info!( + "Loaded global config from: {}", + global_config_path.display() + ); + return Ok(ConfigSource { config }); + } + Err(e) => { + debug!("Failed to load global config: {}", e); + } + } + } + } + + // Use default configuration + if !quiet { + eprintln!( + "{}{}{} Using default configuration", + "[".bold().white(), + "i".bold().blue(), + "]".bold().white(), + ); + } + info!("Using default configuration"); + + Ok(ConfigSource { + config: TomlConfig::default(), + }) +} + +/// Load TOML configuration from a file +fn load_config_from_file(path: &PathBuf) -> Result { + let content = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read config file: {}", path.display()))?; + + TomlConfig::from_toml_str(&content) + .with_context(|| format!("Failed to parse TOML config file: {}", path.display())) +} + +/// Get the default output destination from config +pub fn get_default_output_destination(config_source: &ConfigSource) -> OutputDestination { + config_source.config.default_output.clone() +} diff --git a/crates/code2prompt/src/main.rs b/crates/code2prompt/src/main.rs index cfa5ce2..9347d3d 100644 --- a/crates/code2prompt/src/main.rs +++ b/crates/code2prompt/src/main.rs @@ -4,6 +4,7 @@ mod args; mod clipboard; mod config; +mod config_loader; mod model; mod token_map; mod tui; @@ -14,14 +15,11 @@ mod widgets; use anyhow::{Context, Result}; use args::Cli; use clap::Parser; -use code2prompt_core::{ - session::Code2PromptSession, template::write_to_file, tokenizer::TokenFormat, -}; +use code2prompt_core::{template::write_to_file, tokenizer::TokenFormat}; use colored::*; use indicatif::{ProgressBar, ProgressStyle}; use log::{debug, error, info}; use num_format::{SystemLocale, ToFormattedString}; -use std::io::IsTerminal; use std::io::Write; use tui::run_tui_with_args; @@ -32,13 +30,6 @@ async fn main() -> Result<()> { let args: Cli = Cli::parse(); - // ~~~ Arguments Validation ~~~ - // if no_clipboard is true, output_file must be specified. - if args.no_clipboard && args.output_file.is_none() { - error!("Error: --output-file is required when --no-clipboard is used."); - std::process::exit(1); - } - // ~~~ Clipboard Daemon ~~~ #[cfg(target_os = "linux")] { @@ -51,28 +42,60 @@ async fn main() -> Result<()> { } } - // ~~~ Build Session ~~~ - let mut session = config::create_session_from_args(&args, args.tui).unwrap_or_else(|e| { - error!("Failed to create session: {}", e); - std::process::exit(1); - }); - // ~~~ TUI or CLI Mode ~~~ if args.tui { + // ~~~ Build Session for TUI ~~~ + let session = config::create_session_from_args(&args, args.tui).unwrap_or_else(|e| { + error!("Failed to create session: {}", e); + std::process::exit(1); + }); run_tui_with_args(session).await } else { - run_cli_mode_with_args(args, &mut session).await + run_cli_mode_with_args(args).await } } /// Run the CLI mode with parsed arguments -async fn run_cli_mode_with_args(args: Cli, session: &mut Code2PromptSession) -> Result<()> { - // ~~~ Consolidate Arguments ~~~ - let effective_output = args.output_file.clone(); - // Disable clipboard when outputting to stdout (unless clipboard is explicitly enabled) - let no_clipboard = args.no_clipboard || effective_output.as_ref().is_some_and(|f| f == "-"); - let is_terminal = std::io::stdout().is_terminal(); - let quiet_mode = args.quiet || !is_terminal; +async fn run_cli_mode_with_args(args: Cli) -> Result<()> { + use code2prompt_core::configuration::OutputDestination; + use config_loader::{get_default_output_destination, load_config}; + + let quiet_mode = args.quiet; + + // ~~~ Load Configuration ~~~ + // Always load config files first (local > global), then apply CLI args on top + let config_source = load_config(quiet_mode)?; + + // ~~~ Build Session with config + CLI args ~~~ + let mut session = config::create_session_from_config_and_args(&config_source, &args, false)?; + + // ~~~ Determine Output Behavior ~~~ + let default_output = get_default_output_destination(&config_source); + + // Determine final output destinations (Solution B: Unix-style behavior) + let output_to_clipboard = if args.clipboard { + // Explicit clipboard flag - ONLY clipboard, no stdout + true + } else if args.output_file.is_some() { + // Output file specified, don't use clipboard unless explicitly requested + false + } else { + // Use config default + matches!(default_output, OutputDestination::Clipboard) + }; + + let output_to_stdout = if args.clipboard { + // When -c is used, ONLY output to clipboard, not stdout + false + } else if let Some(ref output_file) = args.output_file { + output_file == "-" + } else { + match default_output { + OutputDestination::Stdout => true, + OutputDestination::Clipboard => false, + OutputDestination::File => false, + } + }; // ~~~ Create Session ~~~ let spinner = if !quiet_mode { @@ -212,8 +235,16 @@ async fn run_cli_mode_with_args(args: Cli, session: &mut Code2PromptSession) -> } } + // ~~~ Output to Stdout (NEW DEFAULT BEHAVIOR) ~~~ + if output_to_stdout { + print!("{}", &rendered.prompt); + std::io::stdout() + .flush() + .context("Failed to flush stdout")?; + } + // ~~~ Copy to Clipboard ~~~ - if !no_clipboard { + if output_to_clipboard { use crate::clipboard::copy_to_clipboard; match copy_to_clipboard(&rendered.prompt) { Ok(_) => { @@ -237,18 +268,21 @@ async fn run_cli_mode_with_args(args: Cli, session: &mut Code2PromptSession) -> format!("Failed to copy to clipboard: {}", e).red() ); } - // optional: fallback - println!("{}", &rendered.prompt); } } } // ~~~ Output File ~~~ - output_prompt( - effective_output.as_deref().map(std::path::Path::new), - &rendered.prompt, - !quiet_mode, - )?; + if let Some(ref output_file) = args.output_file + && output_file != "-" + { + // Output to file (not stdout) + output_prompt( + Some(std::path::Path::new(output_file)), + &rendered.prompt, + quiet_mode, + )?; + } Ok(()) } diff --git a/crates/code2prompt/src/token_map.rs b/crates/code2prompt/src/token_map.rs index 75e5d29..6cb5fe6 100644 --- a/crates/code2prompt/src/token_map.rs +++ b/crates/code2prompt/src/token_map.rs @@ -511,12 +511,11 @@ pub fn display_token_map(entries: &[TokenMapEntry], total_tokens: usize) { name_with_padding }; - // Print the line - println!( + eprintln!( "{:>width$} {}{} │{}│ {}", tokens_str, prefix, - colored_name_with_padding, // This now includes the padding + colored_name_with_padding, bar, percentage_str, width = max_token_width diff --git a/crates/code2prompt/tests/clipboard_test.rs b/crates/code2prompt/tests/clipboard_test.rs deleted file mode 100644 index 45362d1..0000000 --- a/crates/code2prompt/tests/clipboard_test.rs +++ /dev/null @@ -1,24 +0,0 @@ -// #[cfg(test)] -// mod clipboard_tests { -// use arboard::Clipboard; -// use code2prompt::copy_text_to_clipboard; - -// /// Test du fonctionnement de la copie dans le presse-papiers sur les OS non-Linux -// /// -// /// Ce test utilise la fonction simple `copy_to_clipboard` qui définit directement le contenu. -// #[test] -// fn test_clipboard_copy() { -// let test_content = "Test Clipboard Content for non-Linux"; - -// // Utilise la fonction standard pour copier dans le presse-papiers. -// copy_text_to_clipboard(test_content).expect("Failed to copy to clipboard"); - -// // Vérifie que le contenu du presse-papiers correspond. -// let mut clipboard = Clipboard::new().expect("Failed to open clipboard"); -// let content = clipboard.get_text().expect("Failed to get clipboard text"); -// assert_eq!( -// content, test_content, -// "Le contenu du presse-papiers doit être celui de test_content" -// ); -// } -// } diff --git a/crates/code2prompt/tests/common/mod.rs b/crates/code2prompt/tests/common/mod.rs index 06a0f26..74f2762 100644 --- a/crates/code2prompt/tests/common/mod.rs +++ b/crates/code2prompt/tests/common/mod.rs @@ -6,7 +6,6 @@ pub mod fixtures; pub mod test_env; -pub use fixtures::*; pub use test_env::*; use std::sync::Once; diff --git a/crates/code2prompt/tests/config_test.rs b/crates/code2prompt/tests/config_test.rs new file mode 100644 index 0000000..6cb78b4 --- /dev/null +++ b/crates/code2prompt/tests/config_test.rs @@ -0,0 +1,243 @@ +//! Tests for TOML configuration functionality +//! +//! This module tests the TOML configuration loading, parsing, and integration +//! with the new Unix-style behavior. + +mod common; + +use assert_cmd::Command; +use common::*; +use predicates::prelude::*; +use predicates::str::contains; +use std::fs; +use tempfile::TempDir; + +/// Test TOML configuration parsing +#[test] +fn test_toml_config_parsing() { + let toml_content = r#" +default_output = "clipboard" +path = "./src" +include_patterns = ["*.rs", "*.toml"] +exclude_patterns = ["target", "node_modules"] +line_numbers = true +absolute_path = false +full_directory_tree = false +output_format = "markdown" +sort_method = "name_asc" +encoding = "cl100k" +token_format = "format" +diff_enabled = true +diff_branches = ["main", "feature-x"] +log_branches = ["v1.0.0", "v1.1.0"] +template_name = "default" +template_str = "" +token_map_enabled = true + +[user_variables] +project = "code2prompt" +author = "ODAncona" +"#; + + use code2prompt_core::configuration::TomlConfig; + let config = TomlConfig::from_toml_str(toml_content).expect("Should parse TOML config"); + + assert_eq!( + config.default_output, + code2prompt_core::configuration::OutputDestination::Clipboard + ); + assert_eq!(config.path, Some("./src".to_string())); + assert_eq!(config.include_patterns, vec!["*.rs", "*.toml"]); + assert_eq!(config.exclude_patterns, vec!["target", "node_modules"]); + assert!(config.line_numbers); + assert!(!config.absolute_path); + assert!(!config.full_directory_tree); + assert_eq!(config.output_format, Some("markdown".to_string())); + assert_eq!(config.sort_method, Some("name_asc".to_string())); + assert_eq!(config.encoding, Some("cl100k".to_string())); + assert_eq!(config.token_format, Some("format".to_string())); + assert!(config.diff_enabled); + assert_eq!( + config.diff_branches, + Some(vec!["main".to_string(), "feature-x".to_string()]) + ); + assert_eq!( + config.log_branches, + Some(vec!["v1.0.0".to_string(), "v1.1.0".to_string()]) + ); + assert_eq!(config.template_name, Some("default".to_string())); + assert!(config.token_map_enabled); + assert_eq!( + config.user_variables.get("project"), + Some(&"code2prompt".to_string()) + ); + assert_eq!( + config.user_variables.get("author"), + Some(&"ODAncona".to_string()) + ); +} + +/// Test TOML config export functionality +#[test] +fn test_toml_config_export() { + use code2prompt_core::configuration::{Code2PromptConfig, export_config_to_toml}; + + let config = Code2PromptConfig::builder() + .path("./test") + .include_patterns(vec!["*.rs".to_string()]) + .exclude_patterns(vec!["target".to_string()]) + .line_numbers(true) + .build() + .unwrap(); + + let toml_str = export_config_to_toml(&config).expect("Should export to TOML"); + + // Verify the exported TOML contains expected values + assert!(toml_str.contains("default_output = \"stdout\"")); + assert!(toml_str.contains("path = \"./test\"")); + assert!(toml_str.contains("include_patterns = [\"*.rs\"]")); + assert!(toml_str.contains("exclude_patterns = [\"target\"]")); + assert!(toml_str.contains("line_numbers = true")); +} + +/// Test local config file loading +#[test] +fn test_local_config_file_loading() { + let temp_dir = TempDir::new().expect("Should create temp dir"); + let config_path = temp_dir.path().join(".c2pconfig"); + + let toml_content = r#" +default_output = "stdout" +include_patterns = ["*.rs"] +line_numbers = true +"#; + + fs::write(&config_path, toml_content).expect("Should write config file"); + + // Change to the temp directory + let original_dir = std::env::current_dir().expect("Should get current dir"); + std::env::set_current_dir(temp_dir.path()).expect("Should change dir"); + + // Test that the config is loaded (we can't easily test the actual loading here + // without more complex setup, but we can test the file exists) + assert!(config_path.exists()); + + // Restore original directory + std::env::set_current_dir(original_dir).expect("Should restore dir"); +} + +/// Test new Unix-style default behavior (stdout) +#[test] +fn test_unix_style_default_stdout() { + let temp_dir = TempDir::new().expect("Should create temp dir"); + + // Create a test.py file with expected content + fs::write(temp_dir.path().join("test.py"), "print('Hello, World!')") + .expect("Should write test file"); + + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + let temp_path = temp_dir.path().to_path_buf(); + cmd.arg(&temp_path) + .assert() + .success() + .stdout(contains("test.py")) + .stdout(contains("print('Hello, World!')")); + + // Keep temp_dir alive until the end + drop(temp_dir); +} + +/// Test new clipboard flag +#[test] +fn test_clipboard_flag() { + let test_env = StdoutTestEnv::new(); + + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.arg(test_env.path()) + .arg("-c") // New clipboard flag + .assert() + .success() + // Should not output to stdout when using clipboard + .stdout(contains("test.py").not()); +} + +/// Test that CLI args override config files +#[test] +fn test_cli_args_override_config() { + let temp_dir = TempDir::new().expect("Should create temp dir"); + let config_path = temp_dir.path().join(".c2pconfig"); + + // Create a config that would normally exclude .py files + let toml_content = r#" +default_output = "clipboard" +exclude_patterns = ["*.py"] +"#; + + fs::write(&config_path, toml_content).expect("Should write config file"); + fs::write(temp_dir.path().join("test.py"), "print('Hello')").expect("Should write test file"); + + // CLI args should override config - include .py files despite config + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.current_dir(temp_dir.path()) + .arg(".") + .arg("-i") + .arg("*.py") // CLI override + .arg("-O") + .arg("-") // Force output to stdout to see the result + .assert() + .success() + .stdout(contains("test.py")) + .stdout(contains("print('Hello')")); +} + +/// Test configuration info messages +#[test] +fn test_config_info_messages() { + let temp_dir = TempDir::new().expect("Should create temp dir"); + let config_path = temp_dir.path().join(".c2pconfig"); + + let toml_content = r#" +default_output = "stdout" +"#; + + fs::write(&config_path, toml_content).expect("Should write config file"); + fs::write(temp_dir.path().join("test.txt"), "content").expect("Should write test file"); + + // Run with the temp directory as argument and set current directory for the command + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.current_dir(temp_dir.path()) + .arg(".") + .assert() + .success() + .stderr(contains("[i] Using config from:")); +} + +/// Test default configuration message +#[test] +fn test_default_config_message() { + let temp_dir = TempDir::new().expect("Should create temp dir"); + fs::write(temp_dir.path().join("test.txt"), "content").expect("Should write test file"); + + // Run with the temp directory as argument and set current directory for the command + // No config file exists, so it should use default configuration + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.current_dir(temp_dir.path()) + .arg(".") + .assert() + .success() + .stderr(contains("[i] Using default configuration")); +} + +/// Test CLI args message - now CLI args are applied on top of config +#[test] +fn test_cli_args_message() { + let test_env = StdoutTestEnv::new(); + + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.arg(test_env.path()) + .arg("-i") + .arg("*.py") + .assert() + .success() + .stderr(contains("[i] Using default configuration")); // Now always loads config first +} diff --git a/crates/code2prompt/tests/git_integration_test.rs b/crates/code2prompt/tests/git_integration_test.rs index ec5fb54..317d5c0 100644 --- a/crates/code2prompt/tests/git_integration_test.rs +++ b/crates/code2prompt/tests/git_integration_test.rs @@ -5,6 +5,7 @@ mod common; +use common::fixtures::*; use common::*; use log::debug; use predicates::prelude::*; diff --git a/crates/code2prompt/tests/integration_test.rs b/crates/code2prompt/tests/integration_test.rs index 8c1c0ec..220d134 100644 --- a/crates/code2prompt/tests/integration_test.rs +++ b/crates/code2prompt/tests/integration_test.rs @@ -5,6 +5,7 @@ mod common; +use common::fixtures::*; use common::*; use log::debug; use predicates::prelude::*; diff --git a/crates/code2prompt/tests/std_output_test.rs b/crates/code2prompt/tests/std_output_test.rs index b2aae85..bbbf12f 100644 --- a/crates/code2prompt/tests/std_output_test.rs +++ b/crates/code2prompt/tests/std_output_test.rs @@ -6,32 +6,37 @@ mod common; use assert_cmd::Command; +use common::fixtures::*; use common::*; use log::debug; use predicates::prelude::*; use predicates::str::contains; use rstest::*; -/// Test default stdout behavior (auto-quiet when non-terminal) +/// ~~~ Default Output Behavior ~~~ #[rstest] -fn test_stdout_output_default(stdout_test_env: StdoutTestEnv) { - // When stdout is captured by the test harness (non-terminal), auto-quiet suppresses status messages. - // Ensure the command succeeds and does not print status messages. +fn test_output_default(stdout_test_env: StdoutTestEnv) { + // Default behavior: output to stdout with status messages in stderr let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); cmd.arg(stdout_test_env.path()) .assert() .success() - .stdout(contains("Token count:").not()) - .stdout(contains("Copied to clipboard successfully").not()); + // Content should be in stdout + .stdout(contains("test.py")) + .stdout(contains("print('Hello, World!')")) + // Status messages should be in stderr + .stderr(contains("Token count:")) + // Status messages should NOT be in stdout + .stdout(contains("Token count:").not()); debug!("✓ Default stdout output test passed"); } -/// Test various stdout output configurations +/// ~~~ Stdout Configurations ~~~ #[rstest] -#[case("explicit_dash", vec!["-O", "-", "--no-clipboard"], vec!["test.py", "print('Hello, World!')", "README.md", "# Test Project"], vec![], true)] -#[case("long_form", vec!["--output-file", "-", "--no-clipboard"], vec!["test.py", "print('Hello, World!')", "README.md", "# Test Project"], vec![], true)] -#[case("quiet_mode", vec!["--quiet", "-O", "-", "--no-clipboard"], vec!["test.py", "print('Hello, World!')"], vec!["✓"], true)] +#[case("explicit_dash", vec!["-O", "-", "--no-clipboard"], vec!["test.py", "print('Hello, World!')", "README.md", "# Test Project"], vec!["✓","▹▹▹▹▸ Done!","Token count:","Copied to clipboard successfully"], true)] +#[case("long_form", vec!["--output-file", "-", "--no-clipboard"], vec!["test.py", "print('Hello, World!')", "README.md", "# Test Project"], vec!["✓","▹▹▹▹▸ Done!","Token count:","Copied to clipboard successfully"], true)] +#[case("quiet_mode", vec!["--quiet", "-O", "-", "--no-clipboard"], vec!["test.py", "print('Hello, World!')"], vec!["✓","▹▹▹▹▸ Done!","Token count:","Copied to clipboard successfully"], true)] fn test_stdout_configurations( stdout_test_env: StdoutTestEnv, #[case] test_name: &str, @@ -70,6 +75,71 @@ fn test_stdout_configurations( } } +/// ~~~ File Output Configurations ~~~ +#[rstest] +#[case("file_output", vec!["--output-file", "output.txt", "--no-clipboard"], vec!["test.py", "print('Hello, World!')", "README.md"], vec![], true)] +#[case("file_output_quiet", vec!["--output-file", "output.txt", "--quiet", "--no-clipboard"], vec!["test.py", "print('Hello, World!')"], vec!["✓"], true)] +#[case("file_output_json", vec!["--output-file", "output.txt", "--output-format", "json", "--no-clipboard"], vec!["{", "\"files\"", "test.py"], vec![], true)] +#[case("file_output_xml", vec!["--output-file", "output.txt", "--output-format", "xml", "--no-clipboard"], vec!["", "", "test.py"], vec![], true)] +#[case("file_output_markdown", vec!["--output-file", "output.txt", "--output-format", "markdown", "--no-clipboard"], vec!["Source Tree:", "```", "test.py"], vec![], true)] +fn test_file_output_configurations( + stdout_test_env: StdoutTestEnv, + #[case] test_name: &str, + #[case] args: Vec<&str>, + #[case] should_contain: Vec<&str>, + #[case] should_not_contain: Vec<&str>, + #[case] should_succeed: bool, +) { + let output_file = stdout_test_env.dir.path().join("output.txt"); + + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.arg(stdout_test_env.path()); + + // Replace "output.txt" in args with the actual path + for arg in args { + if arg == "output.txt" { + cmd.arg(output_file.to_str().unwrap()); + } else { + cmd.arg(arg); + } + } + + let assertion = cmd.assert(); + + if should_succeed { + assertion.success(); + + // Read the output file and check its contents + let file_content = + std::fs::read_to_string(&output_file).expect("Should be able to read output file"); + + // Check content that should be present + for content in should_contain { + assert!( + file_content.contains(content), + "Test {}: Expected '{}' in file output", + test_name, + content + ); + } + + // Check content that should not be present + for content in should_not_contain { + assert!( + !file_content.contains(content), + "Test {}: Expected '{}' NOT to be in file output", + test_name, + content + ); + } + + debug!("✓ {} test passed", test_name); + } else { + assertion.failure(); + debug!("✓ {} test passed (correctly failed)", test_name); + } +} + /// Test conflicting output options (should fail) #[rstest] fn test_conflicting_output_options_should_fail(stdout_test_env: StdoutTestEnv) { @@ -87,12 +157,11 @@ fn test_conflicting_output_options_should_fail(stdout_test_env: StdoutTestEnv) { debug!("✓ Conflicting output options test passed (correctly failed)"); } -/// Test output file vs stdout conflict (should fail) +// Using both output file and stdout should fail #[rstest] fn test_output_file_vs_stdout_conflict(stdout_test_env: StdoutTestEnv) { let output_file = stdout_test_env.dir.path().join("output.txt"); - // Test: Using both output file and stdout should fail let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); cmd.arg(stdout_test_env.path()) .arg("--output-file") @@ -111,22 +180,6 @@ fn test_output_file_vs_stdout_conflict(stdout_test_env: StdoutTestEnv) { debug!("✓ Output file vs stdout conflict test passed (correctly failed)"); } -/// Test that --no-clipboard requires --output-file -#[rstest] -fn test_no_clipboard_requires_output_file(stdout_test_env: StdoutTestEnv) { - // Test: When --no-clipboard is used, --output-file is required - let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); - cmd.arg(stdout_test_env.path()) - .arg("--no-clipboard") - .assert() - .failure() - .stderr(contains( - "--output-file is required when --no-clipboard is used", - )); - - debug!("✓ No clipboard requires output file test passed"); -} - /// Test stdout with different output formats #[rstest] #[case("json", "{", "\"files\"")] @@ -155,6 +208,104 @@ fn test_stdout_with_different_formats( debug!("✓ Stdout with {} format test passed", format); } +/// Test stderr messages in normal mode (should show status messages) +#[rstest] +fn test_stderr_messages_normal_mode(stdout_test_env: StdoutTestEnv) { + let output_file = stdout_test_env.dir.path().join("output.txt"); + + // Test with file output in normal mode - should show success message in stderr + // Note: In test environment, auto-quiet is enabled, so Token count might not appear + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.arg(stdout_test_env.path()) + .arg("--output-file") + .arg(output_file.to_str().unwrap()) + .arg("--no-clipboard") + .assert() + .success() + .stderr(contains("Prompt written to file:")); + + debug!("✓ Normal mode stderr messages test passed"); +} + +/// Test stderr messages in quiet mode +#[rstest] +fn test_stderr_messages_quiet_mode(stdout_test_env: StdoutTestEnv) { + let output_file = stdout_test_env.dir.path().join("output.txt"); + + // Test with file output in quiet mode - should still show file write confirmation + // but suppress other messages + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.arg(stdout_test_env.path()) + .arg("--output-file") + .arg(output_file.to_str().unwrap()) + .arg("--quiet") + .arg("--no-clipboard") + .assert() + .success() + .stderr(contains("Done!").not()); + // Note: Even in quiet mode, file write confirmation might still appear + // This is expected behavior for important operations + + debug!("✓ Quiet mode stderr messages test passed"); +} + +/// Test stderr messages with clipboard operations +#[rstest] +fn test_stderr_messages_with_clipboard(stdout_test_env: StdoutTestEnv) { + // Test without --no-clipboard flag - should attempt clipboard operation + // Note: In test environment (non-terminal), auto-quiet is enabled + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.arg(stdout_test_env.path()).assert().success(); + // In test environment, clipboard operations might be silent due to auto-quiet + // This is expected behavior + + debug!("✓ Clipboard stderr messages test passed"); +} + +/// Test stderr behavior with different output formats +#[rstest] +#[case("json")] +#[case("xml")] +#[case("markdown")] +fn test_stderr_with_output_formats(stdout_test_env: StdoutTestEnv, #[case] format: &str) { + let output_file = stdout_test_env.dir.path().join("output.txt"); + + // Test that stderr messages appear regardless of output format + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.arg(stdout_test_env.path()) + .arg("--output-file") + .arg(output_file.to_str().unwrap()) + .arg("--output-format") + .arg(format) + .arg("--no-clipboard") + .assert() + .success() + .stderr(contains("Prompt written to file:")); + + debug!("✓ Stderr with {} format test passed", format); +} + +/// Test that stdout and stderr are properly separated +#[rstest] +fn test_stdout_stderr_separation(stdout_test_env: StdoutTestEnv) { + // Test that when outputting to stdout, status messages go to stderr, not stdout + let mut cmd = Command::cargo_bin("code2prompt").expect("Failed to find code2prompt binary"); + cmd.arg(stdout_test_env.path()) + .arg("-O") + .arg("-") + .arg("--no-clipboard") + .assert() + .success() + // Content should be in stdout + .stdout(contains("test.py")) + .stdout(contains("print('Hello, World!')")) + // Status messages should NOT be in stdout (they go to stderr in non-quiet mode) + .stdout(contains("Token count:").not()) + .stdout(contains("✓").not()); + + debug!("✓ Stdout/stderr separation test passed"); +} + /// Test that fixture creates proper test environment #[rstest] fn test_stdout_fixture_setup(stdout_test_env: StdoutTestEnv) { diff --git a/crates/code2prompt/tests/template_integration_test.rs b/crates/code2prompt/tests/template_integration_test.rs index 76b3f5c..c67aa1c 100644 --- a/crates/code2prompt/tests/template_integration_test.rs +++ b/crates/code2prompt/tests/template_integration_test.rs @@ -5,6 +5,7 @@ mod common; +use common::fixtures::*; use common::*; use log::debug; use predicates::prelude::*;