├── .envrc ├── .gitignore ├── .gitmodules ├── fonts └── Fira_Code_v5.2 │ ├── ttf │ ├── FiraCode-Bold.ttf │ ├── FiraCode-Light.ttf │ ├── FiraCode-Medium.ttf │ ├── FiraCode-Regular.ttf │ ├── FiraCode-Retina.ttf │ └── FiraCode-SemiBold.ttf │ ├── woff │ ├── FiraCode-VF.woff │ ├── FiraCode-Bold.woff │ ├── FiraCode-Light.woff │ ├── FiraCode-Medium.woff │ ├── FiraCode-Regular.woff │ └── FiraCode-SemiBold.woff │ ├── woff2 │ ├── FiraCode-VF.woff2 │ ├── FiraCode-Bold.woff2 │ ├── FiraCode-Light.woff2 │ ├── FiraCode-Medium.woff2 │ ├── FiraCode-Regular.woff2 │ └── FiraCode-SemiBold.woff2 │ ├── variable_ttf │ └── FiraCode-VF.ttf │ ├── fira_code.css │ ├── specimen.html │ └── README.txt ├── install.sh ├── README.md ├── LICENSE ├── lib ├── focus │ ├── style.zig │ ├── language │ │ ├── rust.zig │ │ ├── deno.zig │ │ ├── go.zig │ │ ├── zig.zig │ │ └── generic.zig │ ├── single_line_editor.zig │ ├── child_process.zig │ ├── launcher.zig │ ├── mach_compat.zig │ ├── selector.zig │ ├── buffer_opener.zig │ ├── line_wrapped_buffer.zig │ ├── file_opener.zig │ ├── error_lister.zig │ ├── atlas.zig │ ├── buffer_searcher.zig │ ├── project_file_opener.zig │ ├── project_searcher.zig │ ├── maker.zig │ ├── language.zig │ └── window.zig └── focus.zig ├── shell.nix └── focus.zig /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *zig-* 2 | .direnv 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/glfw"] 2 | path = deps/glfw 3 | url = https://github.com/tiawl/glfw.zig.git 4 | -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/ttf/FiraCode-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/ttf/FiraCode-Bold.ttf -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/ttf/FiraCode-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/ttf/FiraCode-Light.ttf -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff/FiraCode-VF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff/FiraCode-VF.woff -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/ttf/FiraCode-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/ttf/FiraCode-Medium.ttf -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/ttf/FiraCode-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/ttf/FiraCode-Regular.ttf -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/ttf/FiraCode-Retina.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/ttf/FiraCode-Retina.ttf -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff/FiraCode-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff/FiraCode-Bold.woff -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff/FiraCode-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff/FiraCode-Light.woff -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff2/FiraCode-VF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff2/FiraCode-VF.woff2 -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/ttf/FiraCode-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/ttf/FiraCode-SemiBold.ttf -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff/FiraCode-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff/FiraCode-Medium.woff -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff/FiraCode-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff/FiraCode-Regular.woff -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff/FiraCode-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff/FiraCode-SemiBold.woff -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff2/FiraCode-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff2/FiraCode-Bold.woff2 -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff2/FiraCode-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff2/FiraCode-Light.woff2 -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff2/FiraCode-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff2/FiraCode-Medium.woff2 -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/variable_ttf/FiraCode-VF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/variable_ttf/FiraCode-VF.ttf -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff2/FiraCode-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff2/FiraCode-Regular.woff2 -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/woff2/FiraCode-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamii/focus/HEAD/fonts/Fira_Code_v5.2/woff2/FiraCode-SemiBold.woff2 -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | nix-shell --run 'zig build install -Doptimize=ReleaseSafe' 4 | sudo pkill -9 focus; sleep 1 5 | cp ~/bin/focus "/home/jamie/bin/focus-$(date +%Y-%m-%d_%H-%M-%S)" 6 | cp ./zig-out/bin/focus-dev ~/bin/focus 7 | cp ./zig-out/bin/focus-dev ~/bin/focus-root -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A low-latency text editor. 2 | 3 | Not intended to be useful for anyone but me, but perhaps a useful starting point to fork off your own editor. 4 | 5 | ``` sh 6 | nix-shell 7 | zig build run -Doptimize=ReleaseSafe -Dhome-path=/home/jamie/ -Dprojects-file-path=/home/jamie/secret/projects 8 | ``` 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jamie Brandon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/focus/style.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | 6 | pub const background_color = u.Color.hsla(0, 0.0, 0.2, 1.0); 7 | pub const fade_color = u.Color.hsla(0, 0.0, 0.2, 0.4); 8 | pub const status_background_color = u.Color.hsla(0, 0.0, 0.1, 1.0); 9 | pub const text_color = u.Color.hsla(0, 0.0, 0.9, 1.0); 10 | pub const highlight_color = u.Color.hsla(0, 0.0, 0.9, 0.3); 11 | pub const keyword_color = text_color; 12 | pub const comment_color = u.Color.hsla(0, 0.0, 0.6, 1.0); 13 | pub const multi_cursor_color = u.Color.hsla(150, 1.0, 0.5, 1.0); 14 | pub const paren_match_color = u.Color.hsla(150, 1.0, 0.5, 0.3); 15 | 16 | pub fn identColor(ident: []const u8) u.Color { 17 | const hash = @bitReverse(u.deepHash(ident)); 18 | return u.Color.hsla( 19 | @floatFromInt(hash % 359), 20 | 1.0, 21 | 0.8, 22 | 1.0, 23 | ); 24 | } 25 | 26 | pub fn parenColor(level: usize) u.Color { 27 | const hash = @bitReverse(u.deepHash(level)); 28 | return u.Color.hsla( 29 | @floatFromInt(hash % 359), 30 | 1.0, 31 | 0.8, 32 | 1.0, 33 | ); 34 | } 35 | 36 | pub const emphasisRed = u.Color.hsla(0, 1.0, 0.5, 1.0); 37 | pub const emphasisOrange = u.Color.hsla(30, 1.0, 0.5, 1.0); 38 | pub const emphasisGreen = u.Color.hsla(120, 1.0, 0.5, 1.0); 39 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { cross ? false }: 2 | 3 | let 4 | 5 | hostPkgs = import {}; 6 | 7 | armPkgs = import { 8 | system = "aarch64-linux"; 9 | }; 10 | 11 | crossPkgs = import { 12 | overlays = [(self: super: { 13 | inherit (armPkgs) 14 | gcc 15 | mesa 16 | libGL 17 | ; 18 | })]; 19 | crossSystem = hostPkgs.lib.systems.examples.aarch64-multiplatform; 20 | }; 21 | 22 | targetPkgs = if cross then crossPkgs else hostPkgs; 23 | 24 | zig = hostPkgs.stdenv.mkDerivation { 25 | name = "zig"; 26 | src = fetchTarball ( 27 | if (hostPkgs.system == "x86_64-linux") then { 28 | url = "https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz"; 29 | sha256 = "052pfb144qaqvf8vm7ic0p6j4q2krwwx1d6cy38jy2jzkb588gw3"; 30 | } else 31 | throw ("Unknown system " ++ hostPkgs.system) 32 | ); 33 | dontConfigure = true; 34 | dontBuild = true; 35 | installPhase = '' 36 | mkdir -p $out 37 | mv ./* $out/ 38 | mkdir -p $out/bin 39 | mv $out/zig $out/bin 40 | ''; 41 | }; 42 | 43 | in 44 | 45 | hostPkgs.mkShell rec { 46 | buildInputs = [ 47 | zig 48 | hostPkgs.git 49 | hostPkgs.pkg-config 50 | targetPkgs.libGL 51 | targetPkgs.wayland 52 | targetPkgs.libxkbcommon 53 | ]; 54 | LD_LIBRARY_PATH = "${targetPkgs.lib.makeLibraryPath buildInputs}"; 55 | } 56 | -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/fira_code.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Fira Code'; 3 | src: url('woff2/FiraCode-Light.woff2') format('woff2'), 4 | url("woff/FiraCode-Light.woff") format("woff"); 5 | font-weight: 300; 6 | font-style: normal; 7 | } 8 | 9 | @font-face { 10 | font-family: 'Fira Code'; 11 | src: url('woff2/FiraCode-Regular.woff2') format('woff2'), 12 | url("woff/FiraCode-Regular.woff") format("woff"); 13 | font-weight: 400; 14 | font-style: normal; 15 | } 16 | 17 | @font-face { 18 | font-family: 'Fira Code'; 19 | src: url('woff2/FiraCode-Medium.woff2') format('woff2'), 20 | url("woff/FiraCode-Medium.woff") format("woff"); 21 | font-weight: 500; 22 | font-style: normal; 23 | } 24 | 25 | @font-face { 26 | font-family: 'Fira Code'; 27 | src: url('woff2/FiraCode-SemiBold.woff2') format('woff2'), 28 | url("woff/FiraCode-SemiBold.woff") format("woff"); 29 | font-weight: 600; 30 | font-style: normal; 31 | } 32 | 33 | @font-face { 34 | font-family: 'Fira Code'; 35 | src: url('woff2/FiraCode-Bold.woff2') format('woff2'), 36 | url("woff/FiraCode-Bold.woff") format("woff"); 37 | font-weight: 700; 38 | font-style: normal; 39 | } 40 | 41 | @font-face { 42 | font-family: 'Fira Code VF'; 43 | src: url('woff2/FiraCode-VF.woff2') format('woff2-variations'), 44 | url('woff/FiraCode-VF.woff') format('woff-variations'); 45 | /* font-weight requires a range: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide#Using_a_variable_font_font-face_changes */ 46 | font-weight: 300 700; 47 | font-style: normal; 48 | } -------------------------------------------------------------------------------- /lib/focus/language/rust.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../../focus.zig"); 3 | const generic = @import("./generic.zig"); 4 | const u = focus.util; 5 | 6 | pub const State = struct { 7 | allocator: u.Allocator, 8 | generic: generic.State, 9 | 10 | pub fn init(allocator: u.Allocator, source: []const u8) State { 11 | return .{ 12 | .allocator = allocator, 13 | .generic = generic.State.init(allocator, "//", source), 14 | }; 15 | } 16 | 17 | pub fn deinit(self: *State) void { 18 | self.* = undefined; 19 | } 20 | 21 | pub fn updateBeforeChange(self: *State, source: []const u8, delete_range: [2]usize) void { 22 | self.generic.updateBeforeChange(source, delete_range); 23 | } 24 | 25 | pub fn updateAfterChange(self: *State, source: []const u8, insert_range: [2]usize) void { 26 | self.generic.updateAfterChange(source, insert_range); 27 | } 28 | 29 | pub fn toggleMode(self: *State) void { 30 | self.generic.toggleMode(); 31 | } 32 | 33 | pub fn highlight(self: State, source: []const u8, range: [2]usize, colors: []u.Color) void { 34 | self.generic.highlight(source, range, colors); 35 | } 36 | 37 | pub fn format(self: State, frame_allocator: u.Allocator, source: []const u8) ?[]const u8 { 38 | var child_process = std.process.Child.init( 39 | &[_][]const u8{ "setsid", "rustfmt" }, 40 | self.allocator, 41 | ); 42 | 43 | child_process.stdin_behavior = .Pipe; 44 | child_process.stdout_behavior = .Pipe; 45 | child_process.stderr_behavior = .Pipe; 46 | 47 | child_process.spawn() catch |err| 48 | u.panic("Error spawning `rustfmt`: {}", .{err}); 49 | 50 | child_process.stdin.?.writeAll(source) catch |err| 51 | u.panic("Error writing to `rustfmt` stdin: {}", .{err}); 52 | child_process.stdin.?.close(); 53 | child_process.stdin = null; 54 | 55 | var stdout = std.ArrayListUnmanaged(u8).empty; 56 | var stderr = std.ArrayListUnmanaged(u8).empty; 57 | 58 | child_process.collectOutput(frame_allocator, &stdout, &stderr, std.math.maxInt(usize)) catch |err| 59 | u.panic("Error collecting output from `rustfmt`: {}", .{err}); 60 | 61 | const result = child_process.wait() catch |err| 62 | u.panic("Error waiting for `rustfmt`: {}", .{err}); 63 | 64 | if (u.deepEqual(result, .{ .Exited = 0 })) { 65 | return stdout.toOwnedSlice(frame_allocator) catch u.oom(); 66 | } else { 67 | u.warn("`rustfmt` failed: {s}", .{stderr.items}); 68 | return null; 69 | } 70 | } 71 | 72 | pub fn getAddedIndent(self: State, token_ix: usize) usize { 73 | return self.generic.getAddedIndent(token_ix); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /lib/focus/language/deno.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../../focus.zig"); 3 | const generic = @import("./generic.zig"); 4 | const u = focus.util; 5 | 6 | pub const State = struct { 7 | allocator: u.Allocator, 8 | generic: generic.State, 9 | 10 | pub fn init(allocator: u.Allocator, source: []const u8) State { 11 | return .{ 12 | .allocator = allocator, 13 | .generic = generic.State.init(allocator, "//", source), 14 | }; 15 | } 16 | 17 | pub fn deinit(self: *State) void { 18 | self.* = undefined; 19 | } 20 | 21 | pub fn updateBeforeChange(self: *State, source: []const u8, delete_range: [2]usize) void { 22 | self.generic.updateBeforeChange(source, delete_range); 23 | } 24 | 25 | pub fn updateAfterChange(self: *State, source: []const u8, insert_range: [2]usize) void { 26 | self.generic.updateAfterChange(source, insert_range); 27 | } 28 | 29 | pub fn toggleMode(self: *State) void { 30 | self.generic.toggleMode(); 31 | } 32 | 33 | pub fn highlight(self: State, source: []const u8, range: [2]usize, colors: []u.Color) void { 34 | self.generic.highlight(source, range, colors); 35 | } 36 | 37 | pub fn format(self: State, frame_allocator: u.Allocator, source: []const u8) ?[]const u8 { 38 | var child_process = std.process.Child.init( 39 | &[_][]const u8{ "setsid", "deno", "fmt", "-" }, 40 | self.allocator, 41 | ); 42 | 43 | child_process.stdin_behavior = .Pipe; 44 | child_process.stdout_behavior = .Pipe; 45 | child_process.stderr_behavior = .Pipe; 46 | 47 | child_process.spawn() catch |err| 48 | u.panic("Error spawning `deno fmt`: {}", .{err}); 49 | 50 | child_process.stdin.?.writeAll(source) catch |err| 51 | u.panic("Error writing to `deno fmt` stdin: {}", .{err}); 52 | child_process.stdin.?.close(); 53 | child_process.stdin = null; 54 | 55 | var stdout = std.ArrayListUnmanaged(u8).empty; 56 | var stderr = std.ArrayListUnmanaged(u8).empty; 57 | 58 | child_process.collectOutput(frame_allocator, &stdout, &stderr, std.math.maxInt(usize)) catch |err| 59 | u.panic("Error collecting output from `deno fmt`: {}", .{err}); 60 | 61 | const result = child_process.wait() catch |err| 62 | u.panic("Error waiting for `deno fmt`: {}", .{err}); 63 | 64 | if (u.deepEqual(result, .{ .Exited = 0 })) { 65 | return stdout.toOwnedSlice(frame_allocator) catch u.oom(); 66 | } else { 67 | u.warn("`deno fmt` failed: {s}", .{stderr.items}); 68 | return null; 69 | } 70 | } 71 | 72 | pub fn getAddedIndent(self: State, token_ix: usize) usize { 73 | return self.generic.getAddedIndent(token_ix); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /focus.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | 4 | const focus = @import("./lib/focus.zig"); 5 | 6 | pub var gpa = if (builtin.mode == .Debug) 7 | std.heap.GeneralPurposeAllocator(.{ 8 | .never_unmap = false, 9 | }){} 10 | else 11 | null; 12 | 13 | const Action = union(enum) { 14 | Angel, 15 | Request: focus.Request, 16 | }; 17 | 18 | pub fn main() void { 19 | const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator; 20 | var arena = focus.util.ArenaAllocator.init(allocator); 21 | 22 | const args = std.process.argsAlloc(arena.allocator()) catch focus.util.oom(); 23 | 24 | var action: Action = .{ .Request = .CreateEmptyWindow }; 25 | for (args[1..]) |c_arg| { 26 | const arg: []const u8 = c_arg; 27 | if (std.mem.startsWith(u8, arg, "--")) { 28 | if (focus.util.deepEqual(arg, "--angel")) { 29 | action = .Angel; 30 | } else if (focus.util.deepEqual(arg, "--launcher")) { 31 | action = .{ .Request = .CreateLauncherWindow }; 32 | } else { 33 | focus.util.panic("Unrecognized arg: {s}", .{arg}); 34 | } 35 | } else { 36 | const absolute_filename = std.fs.path.resolve(arena.allocator(), &[_][]const u8{arg}) catch focus.util.oom(); 37 | action = .{ .Request = .{ .CreateEditorWindow = absolute_filename } }; 38 | } 39 | } 40 | 41 | const socket_path = focus.util.format(arena.allocator(), "#{s}", .{args[0]}); 42 | const server_socket = focus.createServerSocket(socket_path); 43 | 44 | switch (action) { 45 | .Angel => { 46 | // no daemon (we're probably in a debugger) 47 | if (server_socket.state != .Bound) 48 | focus.util.panic("Couldn't bind server socket", .{}); 49 | focus.run(allocator, server_socket); 50 | }, 51 | .Request => |request| { 52 | // if we successfully bound the socket then we need to create the daemon 53 | if (server_socket.state == .Bound) { 54 | const log_filename = focus.util.format(arena.allocator(), "/tmp/{s}.log", .{std.fs.path.basename(args[0])}); 55 | if (focus.daemonize(log_filename) == .Child) { 56 | focus.run(allocator, server_socket); 57 | // run doesn't return 58 | unreachable; 59 | } 60 | } 61 | 62 | // ask the main process to do something 63 | const client_socket = focus.createClientSocket(); 64 | focus.sendRequest(client_socket, server_socket, request); 65 | 66 | // wait until it's done 67 | const exit_code = focus.waitReply(client_socket); 68 | arena.deinit(); 69 | std.posix.exit(exit_code); 70 | }, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/focus/single_line_editor.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | const Editor = focus.Editor; 8 | const Window = focus.Window; 9 | const style = focus.style; 10 | const Selector = focus.Selector; 11 | const mach_compat = focus.mach_compat; 12 | 13 | pub const SingleLineEditor = struct { 14 | app: *App, 15 | buffer: *Buffer, 16 | editor: *Editor, 17 | 18 | pub fn init(app: *App, init_text: []const u8) SingleLineEditor { 19 | const buffer = Buffer.initEmpty(app, .{ 20 | .enable_completions = false, 21 | }); 22 | const editor = Editor.init(app, buffer, .{ 23 | .show_status_bar = false, 24 | .show_completer = false, 25 | }); 26 | editor.insert(editor.getMainCursor(), init_text); 27 | return SingleLineEditor{ 28 | .app = app, 29 | .buffer = buffer, 30 | .editor = editor, 31 | }; 32 | } 33 | 34 | pub fn deinit(self: *SingleLineEditor) void { 35 | self.editor.deinit(); 36 | self.buffer.deinit(); 37 | } 38 | 39 | pub fn frame(self: *SingleLineEditor, window: *Window, rect: u.Rect, events: []const mach_compat.Event) enum { Changed, Unchanged } { 40 | const prev_text = self.app.dupe(self.getText()); 41 | 42 | // filter out multiline events 43 | var editor_events = u.ArrayList(mach_compat.Event).init(self.app.frame_allocator); 44 | for (events) |event| { 45 | if (event == .key_press) { 46 | if (event.key_press.key == c.GLFW_KEY_ENTER) continue; 47 | if ((event.key_press.mods & c.GLFW_MOD_ALT != 0) and 48 | (event.key_press.key == 'k' or event.key_press.key == 'i')) continue; 49 | } 50 | editor_events.append(event) catch u.oom(); 51 | } 52 | 53 | // run editor 54 | self.editor.frame(window, rect, editor_events.items); 55 | 56 | // remove any sneaky newlines from eg paste 57 | // TODO want to put this between event handling and render 58 | { 59 | var pos: usize = 0; 60 | while (self.buffer.searchForwards(pos, "\n")) |new_pos| { 61 | pos = new_pos; 62 | self.editor.delete(pos, pos + 1); 63 | } 64 | } 65 | 66 | return if (std.mem.eql(u8, prev_text, self.getText())) .Unchanged else .Changed; 67 | } 68 | 69 | pub fn getText(self: *SingleLineEditor) []const u8 { 70 | // TODO should this copy? 71 | return self.buffer.bytes.items; 72 | } 73 | 74 | pub fn setText(self: *SingleLineEditor, text: []const u8) void { 75 | self.buffer.replace(text); 76 | self.editor.clearMark(); 77 | self.editor.collapseCursors(); 78 | self.editor.goBufferEnd(self.editor.getMainCursor()); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /lib/focus/child_process.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | 6 | pub const ChildProcess = struct { 7 | child_process: std.process.Child, 8 | 9 | pub fn init(allocator: u.Allocator, dirname: []const u8, args: []const []const u8) ChildProcess { 10 | const full_args = std.mem.concat(allocator, []const u8, &.{ 11 | &.{"setsid"}, 12 | args, 13 | }) catch u.oom(); 14 | var child_process = std.process.Child.init(full_args, allocator); 15 | child_process.cwd = dirname; 16 | child_process.stdin_behavior = .Ignore; 17 | child_process.stdout_behavior = .Pipe; 18 | child_process.stderr_behavior = .Pipe; 19 | child_process.spawn() catch |err| 20 | u.panic("{} while running args: {s}", .{ err, full_args }); 21 | for (&[_]std.fs.File{ child_process.stdout.?, child_process.stderr.? }) |file| { 22 | _ = std.posix.fcntl( 23 | file.handle, 24 | std.os.linux.F.SETFL, 25 | @intCast(@as(u32, @bitCast(std.os.linux.O{ .NONBLOCK = true }))), 26 | ) catch |err| 27 | u.panic("Err setting pipe nonblock: {}", .{err}); 28 | } 29 | return .{ .child_process = child_process }; 30 | } 31 | 32 | pub fn deinit(self: *ChildProcess) void { 33 | const my_pgid = std.os.linux.syscall1(.getpgid, @as(usize, @bitCast(@as(isize, std.os.linux.getpid())))); 34 | var child_pgid = my_pgid; 35 | // Have to wait for child to finish `setsid` 36 | while (my_pgid == child_pgid) { 37 | child_pgid = std.os.linux.syscall1(.getpgid, @as(usize, @bitCast(@as(isize, self.child_process.id)))); 38 | } 39 | std.posix.kill(-@as(i32, @intCast(child_pgid)), std.posix.SIG.KILL) catch {}; 40 | self.child_process.stdout.?.close(); 41 | self.child_process.stderr.?.close(); 42 | } 43 | 44 | pub fn poll(self: ChildProcess) enum { Running, Finished } { 45 | const wait = std.posix.waitpid(self.child_process.id, std.os.linux.WNOHANG); 46 | return if (wait.id == self.child_process.id) .Finished else .Running; 47 | } 48 | 49 | pub fn read(self: ChildProcess, allocator: u.Allocator) []const u8 { 50 | var bytes = u.ArrayList(u8).initCapacity(allocator, 4096) catch u.oom(); 51 | defer bytes.deinit(); 52 | var start: usize = 0; 53 | for (&[_]std.fs.File{ self.child_process.stdout.?, self.child_process.stderr.? }) |file| { 54 | while (true) { 55 | bytes.expandToCapacity(); 56 | if (file.read(bytes.items[start..])) |num_bytes_read| { 57 | start += num_bytes_read; 58 | if (num_bytes_read == 0) 59 | break; 60 | } else |err| { 61 | switch (err) { 62 | error.WouldBlock => break, 63 | else => u.panic("Err reading pipe: {}", .{err}), 64 | } 65 | } 66 | bytes.ensureTotalCapacity(start + 1) catch u.oom(); 67 | } 68 | } 69 | bytes.shrinkAndFree(start); 70 | return bytes.toOwnedSlice() catch u.oom(); 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /lib/focus/launcher.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | const Editor = focus.Editor; 8 | const SingleLineEditor = focus.SingleLineEditor; 9 | const Window = focus.Window; 10 | const style = focus.style; 11 | const Selector = focus.Selector; 12 | const mach_compat = focus.mach_compat; 13 | 14 | pub const Launcher = struct { 15 | app: *App, 16 | input: SingleLineEditor, 17 | selector: Selector, 18 | exes: []const []const u8, 19 | 20 | pub fn init(app: *App) *Launcher { 21 | const input = SingleLineEditor.init(app, ""); 22 | const selector = Selector.init(app); 23 | 24 | var exes = u.ArrayList([]const u8).init(app.allocator); 25 | { 26 | const result = std.process.Child.run(.{ 27 | .allocator = app.frame_allocator, 28 | .argv = &[_][]const u8{ "fish", "-C", "complete -C ''" }, 29 | .cwd = focus.config.home_path, 30 | .max_output_bytes = 128 * 1024 * 1024, 31 | }) catch |err| u.panic("{} while calling compgen", .{err}); 32 | u.assert(result.term == .Exited and result.term.Exited == 0); 33 | var lines = std.mem.splitScalar(u8, result.stdout, '\n'); 34 | while (lines.next()) |line| { 35 | var it = std.mem.splitScalar(u8, line, '\t'); 36 | const command = it.first(); 37 | exes.append(app.dupe(command)) catch u.oom(); 38 | } 39 | } 40 | 41 | const self = app.allocator.create(Launcher) catch u.oom(); 42 | self.* = Launcher{ 43 | .app = app, 44 | .input = input, 45 | .selector = selector, 46 | .exes = exes.toOwnedSlice() catch u.oom(), 47 | }; 48 | return self; 49 | } 50 | 51 | pub fn deinit(self: *Launcher) void { 52 | for (self.exes) |exe| self.app.allocator.free(exe); 53 | self.app.allocator.free(self.exes); 54 | self.selector.deinit(); 55 | self.input.deinit(); 56 | self.app.allocator.destroy(self); 57 | } 58 | 59 | pub fn frame(self: *Launcher, window: *Window, rect: u.Rect, events: []const mach_compat.Event) void { 60 | const layout = window.layoutSearcher(rect); 61 | 62 | // run input frame 63 | const input_changed = self.input.frame(window, layout.input, events); 64 | if (input_changed == .Changed) self.selector.selected = 0; 65 | 66 | // filter exes 67 | const filtered_exes = u.fuzzy_search(self.app.frame_allocator, self.exes, self.input.getText()); 68 | 69 | // run selector frame 70 | self.selector.setItems(filtered_exes); 71 | const action = self.selector.frame(window, layout.selector, events); 72 | 73 | // handle action 74 | if (action == .SelectOne) { 75 | const exe = filtered_exes[self.selector.selected]; 76 | // TODO this is kinda hacky :D 77 | const command = u.format(self.app.frame_allocator, "{s} & disown", .{exe}); 78 | var process = std.process.Child.init( 79 | &[_][]const u8{ "fish", "-c", command }, 80 | self.app.frame_allocator, 81 | ); 82 | process.spawn() catch |err| u.panic("Failed to spawn {s}: {}", .{ command, err }); 83 | window.close_after_frame = true; 84 | } 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/specimen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fira Code Specimen 7 | 8 | 9 | 28 | 35 | 36 | 37 |
# Fira Code Light 38 | 39 | take = (n, [x, ...xs]:list) --> 40 | | n <= 0 => [] 41 | | empty list => [] 42 | | otherwise => [x] ++ take n-1, xs 43 | 44 | last3 = reverse >> take 3 >> reverse
45 | 46 | 47 |
# Fira Code Regular 48 | 49 | take = (n, [x, ...xs]:list) --> 50 | | n <= 0 => [] 51 | | empty list => [] 52 | | otherwise => [x] ++ take n-1, xs 53 | 54 | last3 = reverse >> take 3 >> reverse
55 | 56 | 57 |
# Fira Code Medium 58 | 59 | take = (n, [x, ...xs]:list) --> 60 | | n <= 0 => [] 61 | | empty list => [] 62 | | otherwise => [x] ++ take n-1, xs 63 | 64 | last3 = reverse >> take 3 >> reverse
65 | 66 | 67 |
# Fira Code SemiBold 68 | 69 | take = (n, [x, ...xs]:list) --> 70 | | n <= 0 => [] 71 | | empty list => [] 72 | | otherwise => [x] ++ take n-1, xs 73 | 74 | last3 = reverse >> take 3 >> reverse
75 | 76 | 77 |
# Fira Code Bold 78 | 79 | take = (n, [x, ...xs]:list) --> 80 | | n <= 0 => [] 81 | | empty list => [] 82 | | otherwise => [x] ++ take n-1, xs 83 | 84 | last3 = reverse >> take 3 >> reverse
85 | 86 |
# Fira Code Variable 87 | 88 | 400 89 | 90 | take = (n, [x, ...xs]:list) --> 91 | | n <= 0 => [] 92 | | empty list => [] 93 | | otherwise => [x] ++ take n-1, xs 94 | 95 | last3 = reverse >> take 3 >> reverse
96 | -------------------------------------------------------------------------------- /lib/focus/language/go.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../../focus.zig"); 3 | const generic = @import("./generic.zig"); 4 | const u = focus.util; 5 | 6 | pub const State = struct { 7 | allocator: u.Allocator, 8 | generic: generic.State, 9 | 10 | pub fn init(allocator: u.Allocator, source: []const u8) State { 11 | return .{ 12 | .allocator = allocator, 13 | .generic = generic.State.init(allocator, "//", source), 14 | }; 15 | } 16 | 17 | pub fn deinit(self: *State) void { 18 | self.* = undefined; 19 | } 20 | 21 | pub fn updateBeforeChange(self: *State, source: []const u8, delete_range: [2]usize) void { 22 | self.generic.updateBeforeChange(source, delete_range); 23 | } 24 | 25 | pub fn updateAfterChange(self: *State, source: []const u8, insert_range: [2]usize) void { 26 | self.generic.updateAfterChange(source, insert_range); 27 | } 28 | 29 | pub fn toggleMode(self: *State) void { 30 | self.generic.toggleMode(); 31 | } 32 | 33 | pub fn highlight(self: State, source: []const u8, range: [2]usize, colors: []u.Color) void { 34 | self.generic.highlight(source, range, colors); 35 | } 36 | 37 | fn formatWithTabs(self: State, frame_allocator: u.Allocator, source: []const u8) ?[]const u8 { 38 | _ = self; 39 | 40 | var child_process = std.process.Child.init( 41 | &[_][]const u8{ "setsid", "gofmt", "-s" }, 42 | frame_allocator, 43 | ); 44 | 45 | child_process.stdin_behavior = .Pipe; 46 | child_process.stdout_behavior = .Pipe; 47 | child_process.stderr_behavior = .Pipe; 48 | 49 | child_process.spawn() catch |err| 50 | u.panic("Error spawning `gofmt`: {}", .{err}); 51 | 52 | child_process.stdin.?.writeAll(source) catch |err| 53 | u.panic("Error writing to `gofmt` stdin: {}", .{err}); 54 | child_process.stdin.?.close(); 55 | child_process.stdin = null; 56 | 57 | var stdout = std.ArrayListUnmanaged(u8).empty; 58 | var stderr = std.ArrayListUnmanaged(u8).empty; 59 | 60 | child_process.collectOutput(frame_allocator, &stdout, &stderr, std.math.maxInt(usize)) catch |err| 61 | u.panic("Error collecting output from `gofmt`: {}", .{err}); 62 | 63 | const result = child_process.wait() catch |err| 64 | u.panic("Error waiting for `gofmt`: {}", .{err}); 65 | 66 | if (u.deepEqual(result, .{ .Exited = 0 })) { 67 | return stdout.toOwnedSlice(frame_allocator) catch u.oom(); 68 | } else { 69 | u.warn("`gofmt` failed: {s}", .{stderr.items}); 70 | return null; 71 | } 72 | } 73 | 74 | pub fn format(self: State, frame_allocator: u.Allocator, source: []const u8) ?[]const u8 { 75 | return if (self.formatWithTabs(frame_allocator, source)) |new_source| 76 | self.afterLoad(frame_allocator, new_source) 77 | else 78 | null; 79 | } 80 | 81 | pub fn getAddedIndent(self: State, token_ix: usize) usize { 82 | return self.generic.getAddedIndent(token_ix); 83 | } 84 | 85 | pub fn afterLoad(self: State, frame_allocator: u.Allocator, source: []const u8) []const u8 { 86 | _ = self; 87 | 88 | var replaced = u.ArrayList(u8).initCapacity(frame_allocator, source.len) catch u.oom(); 89 | for (source) |char| { 90 | if (char == '\t') { 91 | replaced.appendNTimes(' ', 4) catch u.oom(); 92 | } else { 93 | replaced.append(char) catch u.oom(); 94 | } 95 | } 96 | return replaced.items; 97 | } 98 | 99 | pub fn beforeSave(self: State, frame_allocator: u.Allocator, source: []const u8) []const u8 { 100 | return self.formatWithTabs(frame_allocator, source) orelse source; 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /lib/focus/mach_compat.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | 6 | pub const Event = union(enum) { 7 | key_press: KeyEvent, 8 | key_repeat: KeyEvent, 9 | key_release: KeyEvent, 10 | char_input: struct { 11 | codepoint: u21, 12 | }, 13 | mouse_motion: struct { 14 | x: f64, 15 | y: f64, 16 | }, 17 | mouse_press: MouseButtonEvent, 18 | mouse_release: MouseButtonEvent, 19 | mouse_scroll: struct { 20 | xoffset: f64, 21 | yoffset: f64, 22 | }, 23 | focus_gained, 24 | focus_lost, 25 | window_closed, 26 | }; 27 | 28 | pub const KeyEvent = struct { 29 | key: c_int, 30 | mods: c_int, 31 | }; 32 | 33 | pub const MouseButtonEvent = struct { 34 | button: c_int, 35 | pos: [2]f64, 36 | mods: c_int, 37 | }; 38 | 39 | fn getEventsList(window: ?*c.GLFWwindow) *u.ArrayList(Event) { 40 | return @ptrCast(@alignCast(c.glfwGetWindowUserPointer(window))); 41 | } 42 | 43 | fn keyCallback(window: ?*c.GLFWwindow, key: c_int, scancode: c_int, action: c_int, mods: c_int) callconv(.C) void { 44 | const events = getEventsList(window); 45 | const key_event = KeyEvent{ 46 | .key = key, 47 | .mods = mods, 48 | }; 49 | const event = switch (action) { 50 | c.GLFW_PRESS => Event{ .key_press = key_event }, 51 | c.GLFW_REPEAT => Event{ .key_repeat = key_event }, 52 | c.GLFW_RELEASE => Event{ .key_release = key_event }, 53 | else => u.panic("Unexpected action: {}", .{action}), 54 | }; 55 | events.append(event) catch u.oom(); 56 | _ = scancode; 57 | } 58 | 59 | fn mouseMotionCallback(window: ?*c.GLFWwindow, xpos: f64, ypos: f64) callconv(.C) void { 60 | const events = getEventsList(window); 61 | events.append(.{ 62 | .mouse_motion = .{ 63 | .x = xpos, 64 | .y = ypos, 65 | }, 66 | }) catch u.oom(); 67 | } 68 | 69 | fn mouseButtonCallback(window: ?*c.GLFWwindow, button: c_int, action: c_int, mods: c_int) callconv(.C) void { 70 | const events = getEventsList(window); 71 | var cursor_pos: [2]f64 = .{ 0, 0 }; 72 | c.glfwGetCursorPos(window, &cursor_pos[0], &cursor_pos[1]); 73 | const mouse_button_event = MouseButtonEvent{ 74 | .button = button, 75 | .pos = cursor_pos, 76 | .mods = mods, 77 | }; 78 | const event = switch (action) { 79 | c.GLFW_PRESS => Event{ .mouse_press = mouse_button_event }, 80 | c.GLFW_RELEASE => Event{ .mouse_release = mouse_button_event }, 81 | else => u.panic("Unexpected action: {}", .{action}), 82 | }; 83 | events.append(event) catch u.oom(); 84 | } 85 | 86 | fn scrollCallback(window: ?*c.GLFWwindow, xoffset: f64, yoffset: f64) callconv(.C) void { 87 | const events = getEventsList(window); 88 | events.append(.{ 89 | .mouse_scroll = .{ 90 | .xoffset = xoffset, 91 | .yoffset = yoffset, 92 | }, 93 | }) catch u.oom(); 94 | } 95 | 96 | fn focusCallback(window: ?*c.GLFWwindow, focused: c_int) callconv(.C) void { 97 | const events = getEventsList(window); 98 | events.append(if (focused == c.GLFW_TRUE) .focus_gained else .focus_lost) catch u.oom(); 99 | } 100 | 101 | fn closeCallback(window: ?*c.GLFWwindow) callconv(.C) void { 102 | const events = getEventsList(window); 103 | events.append(.window_closed) catch u.oom(); 104 | } 105 | 106 | fn charCallback(window: ?*c.GLFWwindow, codepoint: c_uint) callconv(.C) void { 107 | const events = getEventsList(window); 108 | events.append(.{ 109 | .char_input = .{ 110 | .codepoint = @intCast(codepoint), 111 | }, 112 | }) catch u.oom(); 113 | } 114 | 115 | pub fn setCallbacks(window: ?*c.GLFWwindow, events: *u.ArrayList(Event)) callconv(.C) void { 116 | _ = c.glfwSetWindowUserPointer(window, events); 117 | _ = c.glfwSetKeyCallback(window, keyCallback); 118 | _ = c.glfwSetCursorPosCallback(window, mouseMotionCallback); 119 | _ = c.glfwSetMouseButtonCallback(window, mouseButtonCallback); 120 | _ = c.glfwSetScrollCallback(window, scrollCallback); 121 | _ = c.glfwSetWindowFocusCallback(window, focusCallback); 122 | _ = c.glfwSetWindowCloseCallback(window, closeCallback); 123 | _ = c.glfwSetCharCallback(window, charCallback); 124 | } 125 | -------------------------------------------------------------------------------- /lib/focus/selector.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | const Editor = focus.Editor; 8 | const Window = focus.Window; 9 | const mach_compat = focus.mach_compat; 10 | 11 | pub const Selector = struct { 12 | app: *App, 13 | buffer: *Buffer, 14 | editor: *Editor, 15 | selected: usize, 16 | ranges: []const [2]usize, 17 | 18 | pub const Action = enum { 19 | None, 20 | SelectRaw, 21 | SelectOne, 22 | SelectAll, 23 | }; 24 | 25 | pub fn init(app: *App) Selector { 26 | const buffer = Buffer.initEmpty(app, .{ 27 | .enable_completions = false, 28 | .enable_undo = false, 29 | }); 30 | const editor = Editor.init(app, buffer, .{ 31 | .show_status_bar = false, 32 | .show_completer = false, 33 | }); 34 | return Selector{ 35 | .app = app, 36 | .buffer = buffer, 37 | .editor = editor, 38 | .selected = 0, 39 | .ranges = &.{}, 40 | }; 41 | } 42 | 43 | pub fn deinit(self: *Selector) void { 44 | self.app.allocator.free(self.ranges); 45 | self.editor.deinit(); 46 | self.buffer.deinit(); 47 | } 48 | 49 | pub fn setItems(self: *Selector, items: []const []const u8) void { 50 | var text = u.ArrayList(u8).init(self.app.frame_allocator); 51 | var ranges = u.ArrayList([2]usize).init(self.app.frame_allocator); 52 | for (items) |item| { 53 | const start = text.items.len; 54 | text.appendSlice(item) catch u.oom(); 55 | const end = text.items.len; 56 | text.append('\n') catch u.oom(); 57 | ranges.append(.{ start, end }) catch u.oom(); 58 | } 59 | self.setTextAndRanges(text.toOwnedSlice() catch u.oom(), ranges.toOwnedSlice() catch u.oom()); 60 | } 61 | 62 | pub fn setTextAndRanges(self: *Selector, text: []const u8, ranges: []const [2]usize) void { 63 | self.buffer.replace(text); 64 | self.setRanges(ranges); 65 | } 66 | 67 | pub fn setRanges(self: *Selector, ranges: []const [2]usize) void { 68 | self.app.allocator.free(self.ranges); 69 | self.ranges = self.app.dupe(ranges); 70 | } 71 | 72 | pub fn logic(self: *Selector, events: []const mach_compat.Event, num_items: usize) Action { 73 | var action: Action = .None; 74 | const old_selected = self.selected; 75 | for (events) |event| { 76 | switch (event) { 77 | .key_press, .key_repeat => |key_press_event| { 78 | if (key_press_event.mods & c.GLFW_MOD_CONTROL != 0) { 79 | switch (key_press_event.key) { 80 | c.GLFW_KEY_K => self.selected += 1, 81 | c.GLFW_KEY_I => if (self.selected != 0) { 82 | self.selected -= 1; 83 | }, 84 | c.GLFW_KEY_ENTER => { 85 | action = .SelectRaw; 86 | }, 87 | else => {}, 88 | } 89 | } else if (key_press_event.mods & c.GLFW_MOD_ALT != 0) { 90 | switch (key_press_event.key) { 91 | c.GLFW_KEY_K => self.selected = num_items - 1, 92 | c.GLFW_KEY_I => self.selected = 0, 93 | c.GLFW_KEY_ENTER => { 94 | action = .SelectAll; 95 | }, 96 | else => {}, 97 | } 98 | } else { 99 | switch (key_press_event.key) { 100 | c.GLFW_KEY_ENTER => if (num_items != 0) { 101 | action = .SelectOne; 102 | }, 103 | else => {}, 104 | } 105 | } 106 | }, 107 | else => {}, 108 | } 109 | } 110 | if (old_selected != self.selected) 111 | self.selected = @min(self.selected, @max(1, num_items) - 1); 112 | if (action == .SelectOne and self.selected >= num_items) 113 | action = .None; 114 | return action; 115 | } 116 | 117 | pub fn frame(self: *Selector, window: *Window, rect: u.Rect, events: []const mach_compat.Event) Action { 118 | const action = self.logic(events, self.ranges.len); 119 | 120 | // set selection 121 | const cursor = self.editor.getMainCursor(); 122 | if (self.selected < self.ranges.len) { 123 | self.editor.goPos(cursor, self.ranges[self.selected][0]); 124 | self.editor.setMark(); 125 | self.editor.goPos(cursor, self.ranges[self.selected][1]); 126 | } else { 127 | self.editor.clearMark(); 128 | self.editor.goBufferStart(cursor); 129 | } 130 | 131 | // render 132 | self.editor.frame(window, rect, &[0]mach_compat.Event{}); 133 | 134 | return action; 135 | } 136 | }; 137 | -------------------------------------------------------------------------------- /lib/focus/buffer_opener.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | const Editor = focus.Editor; 8 | const SingleLineEditor = focus.SingleLineEditor; 9 | const Window = focus.Window; 10 | const style = focus.style; 11 | const Selector = focus.Selector; 12 | const mach_compat = focus.mach_compat; 13 | 14 | pub const BufferOpener = struct { 15 | app: *App, 16 | ignore_buffer: ?*Buffer, 17 | preview_editor: *Editor, 18 | input: SingleLineEditor, 19 | selector: Selector, 20 | 21 | pub fn init(app: *App, ignore_buffer: ?*Buffer) *BufferOpener { 22 | const empty_buffer = Buffer.initEmpty(app, .{ 23 | .limit_load_bytes = true, 24 | .enable_completions = false, 25 | .enable_undo = false, 26 | }); 27 | const preview_editor = Editor.init(app, empty_buffer, .{ 28 | .show_status_bar = false, 29 | .show_completer = false, 30 | }); 31 | const input = SingleLineEditor.init(app, ""); 32 | // const input = SingleLineEditor.init(app, app.last_file_filter); 33 | input.editor.goRealLineStart(input.editor.getMainCursor()); 34 | input.editor.setMark(); 35 | input.editor.goRealLineEnd(input.editor.getMainCursor()); 36 | const selector = Selector.init(app); 37 | // selector.selected = app.last_buffer_opener_selected; 38 | 39 | const self = app.allocator.create(BufferOpener) catch u.oom(); 40 | self.* = BufferOpener{ 41 | .app = app, 42 | .ignore_buffer = ignore_buffer, 43 | .preview_editor = preview_editor, 44 | .input = input, 45 | .selector = selector, 46 | }; 47 | return self; 48 | } 49 | 50 | pub fn deinit(self: *BufferOpener) void { 51 | self.selector.deinit(); 52 | self.input.deinit(); 53 | self.preview_editor.deinit(); 54 | self.app.allocator.destroy(self); 55 | } 56 | 57 | pub fn frame(self: *BufferOpener, window: *Window, rect: u.Rect, events: []const mach_compat.Event) void { 58 | const layout = window.layoutSearcherWithPreview(rect); 59 | 60 | // run input frame 61 | const input_changed = self.input.frame(window, layout.input, events); 62 | if (input_changed == .Changed) self.selector.selected = 0; 63 | 64 | // get buffer paths 65 | var paths = u.ArrayList([]const u8).init(self.app.frame_allocator); 66 | { 67 | const Entry = @TypeOf(self.app.buffers).Entry; 68 | var entries = u.ArrayList(Entry).init(self.app.frame_allocator); 69 | var buffers_iter = self.app.buffers.iterator(); 70 | while (buffers_iter.next()) |entry| { 71 | if (entry.value_ptr.* != self.ignore_buffer) 72 | entries.append(entry) catch u.oom(); 73 | } 74 | // sort by most recently focused 75 | std.mem.sort(Entry, entries.items, {}, (struct { 76 | fn lessThan(_: void, a: Entry, b: Entry) bool { 77 | return b.value_ptr.*.last_lost_focus_ms < a.value_ptr.*.last_lost_focus_ms; 78 | } 79 | }).lessThan); 80 | for (entries.items) |entry| { 81 | paths.append(entry.key_ptr.*) catch u.oom(); 82 | } 83 | } 84 | 85 | // filter paths 86 | const filtered_paths = u.fuzzy_search(self.app.frame_allocator, paths.items, self.input.getText()); 87 | 88 | // run selector frame 89 | self.selector.setItems(filtered_paths); 90 | const action = self.selector.frame(window, layout.selector, events); 91 | 92 | // maybe open file 93 | if (action == .SelectOne) { 94 | const path = filtered_paths[self.selector.selected]; 95 | const new_buffer = self.app.getBufferFromAbsoluteFilename(path); 96 | const new_editor = Editor.init(self.app, new_buffer, .{}); 97 | window.popView(); 98 | window.pushView(new_editor); 99 | } 100 | 101 | // set cached search text 102 | self.app.allocator.free(self.app.last_file_filter); 103 | self.app.last_file_filter = self.app.dupe(self.input.getText()); 104 | self.app.last_buffer_opener_selected = self.selector.selected; 105 | 106 | // update preview 107 | self.preview_editor.deinit(); 108 | if (self.selector.selected >= filtered_paths.len) { 109 | const empty_buffer = Buffer.initEmpty(self.app, .{ 110 | .limit_load_bytes = true, 111 | .enable_completions = false, 112 | .enable_undo = false, 113 | }); 114 | self.preview_editor = Editor.init(self.app, empty_buffer, .{ 115 | .show_status_bar = false, 116 | .show_completer = false, 117 | }); 118 | } else { 119 | const selected = filtered_paths[self.selector.selected]; 120 | const preview_buffer = self.app.getBufferFromAbsoluteFilename(selected); 121 | self.preview_editor = Editor.init(self.app, preview_buffer, .{ 122 | .show_status_bar = false, 123 | .show_completer = false, 124 | }); 125 | } 126 | 127 | // run preview frame 128 | self.preview_editor.frame(window, layout.preview, &[0]mach_compat.Event{}); 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /lib/focus/line_wrapped_buffer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | 8 | pub const LineWrappedBuffer = struct { 9 | app: *App, 10 | buffer: *Buffer, 11 | max_chars_per_line: usize, 12 | wrapped_line_ranges: u.ArrayList([2]usize), 13 | 14 | pub fn init(app: *App, buffer: *Buffer, max_chars_per_line: usize) LineWrappedBuffer { 15 | var self = LineWrappedBuffer{ 16 | .app = app, 17 | .buffer = buffer, 18 | .max_chars_per_line = max_chars_per_line, 19 | .wrapped_line_ranges = u.ArrayList([2]usize).init(app.allocator), 20 | }; 21 | self.update(); 22 | return self; 23 | } 24 | 25 | pub fn deinit(self: *LineWrappedBuffer) void { 26 | self.wrapped_line_ranges.deinit(); 27 | } 28 | 29 | pub fn update(self: *LineWrappedBuffer) void { 30 | self.wrapped_line_ranges.shrinkRetainingCapacity(0); 31 | self.updateFromLineRanges(self.buffer.line_ranges.items); 32 | } 33 | 34 | pub fn updateAfterAppend(self: *LineWrappedBuffer, append_pos: usize) void { 35 | const line_ranges = self.buffer.line_ranges.items; 36 | const wrapped_line_ranges = &self.wrapped_line_ranges; 37 | 38 | var line_start_ix = line_ranges.len -| 1; 39 | while (line_start_ix > 0 and line_ranges[line_start_ix][1] > append_pos) : (line_start_ix -= 1) {} 40 | 41 | const start_pos = line_ranges[line_start_ix][0]; 42 | while (wrapped_line_ranges.pop()) |wrapped_line_range| { 43 | if (wrapped_line_range[0] < start_pos) { 44 | wrapped_line_ranges.append(wrapped_line_range) catch u.oom(); 45 | break; 46 | } 47 | } 48 | 49 | self.updateFromLineRanges(line_ranges[line_start_ix..]); 50 | } 51 | 52 | fn updateFromLineRanges(self: *LineWrappedBuffer, line_ranges: []const [2]usize) void { 53 | const bytes = self.buffer.bytes.items; 54 | const wrapped_line_ranges = &self.wrapped_line_ranges; 55 | for (line_ranges) |real_line_range| { 56 | const real_line_end = real_line_range[1]; 57 | var line_start: usize = real_line_range[0]; 58 | if (real_line_end - line_start <= self.max_chars_per_line) { 59 | wrapped_line_ranges.append(real_line_range) catch u.oom(); 60 | continue; 61 | } 62 | while (true) { 63 | var line_end = line_start; 64 | var maybe_line_end = line_end; 65 | var seen_non_whitespace = false; 66 | { 67 | while (true) { 68 | if (maybe_line_end >= real_line_end) { 69 | line_end = maybe_line_end; 70 | break; 71 | } 72 | const char = bytes[maybe_line_end]; 73 | if (maybe_line_end - line_start > self.max_chars_per_line) { 74 | // if we haven't soft wrapped yet, hard wrap before this char, otherwise use soft wrap 75 | if (line_end == line_start) { 76 | line_end = maybe_line_end; 77 | } 78 | break; 79 | } 80 | if (char == '\n') { 81 | // wrap here 82 | line_end = maybe_line_end; 83 | break; 84 | } 85 | maybe_line_end += 1; 86 | if (char == ' ') { 87 | if (seen_non_whitespace) 88 | // commit to including this char 89 | line_end = maybe_line_end; 90 | } else { 91 | seen_non_whitespace = true; 92 | } 93 | // otherwise keep looking ahead 94 | } 95 | } 96 | self.wrapped_line_ranges.append(.{ line_start, line_end }) catch u.oom(); 97 | if (line_end >= real_line_end) break; 98 | line_start = line_end; 99 | } 100 | } 101 | } 102 | 103 | pub fn getLineColForPos(self: *LineWrappedBuffer, pos: usize) [2]usize { 104 | var line = std.sort.binarySearch([2]usize, self.wrapped_line_ranges.items, pos, struct { 105 | fn compare(pos2: usize, item: [2]usize) std.math.Order { 106 | if (pos2 < item[0]) return .lt; 107 | if (pos2 > item[1]) return .gt; 108 | return .eq; 109 | } 110 | }.compare).?; 111 | // check next line to resolve ambiguity around putting the cursor before/after line wraps 112 | if (line + 1 < self.wrapped_line_ranges.items.len and pos == self.wrapped_line_ranges.items[line + 1][0]) line = line + 1; 113 | const line_range = self.wrapped_line_ranges.items[line]; 114 | return .{ line, pos - line_range[0] }; 115 | } 116 | 117 | pub fn getRangeForLine(self: *LineWrappedBuffer, line: usize) [2]usize { 118 | return self.wrapped_line_ranges.items[line]; 119 | } 120 | 121 | pub fn getPosForLine(self: *LineWrappedBuffer, line: usize) usize { 122 | return self.getRangeForLine(line)[0]; 123 | } 124 | 125 | pub fn getPosForLineCol(self: *LineWrappedBuffer, line: usize, col: usize) usize { 126 | const range = self.wrapped_line_ranges.items[line]; 127 | return range[0] + @min(col, range[1] - range[0]); 128 | } 129 | 130 | pub fn getLineStart(self: *LineWrappedBuffer, pos: usize) usize { 131 | const line_col = self.getLineColForPos(pos); 132 | return self.getRangeForLine(line_col[0])[0]; 133 | } 134 | 135 | pub fn getLineEnd(self: *LineWrappedBuffer, pos: usize) usize { 136 | const line_col = self.getLineColForPos(pos); 137 | return self.getRangeForLine(line_col[0])[1]; 138 | } 139 | 140 | pub fn countLines(self: *LineWrappedBuffer) usize { 141 | return self.wrapped_line_ranges.items.len; 142 | } 143 | }; 144 | -------------------------------------------------------------------------------- /lib/focus/file_opener.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | const Editor = focus.Editor; 8 | const Window = focus.Window; 9 | const style = focus.style; 10 | const SingleLineEditor = focus.SingleLineEditor; 11 | const Selector = focus.Selector; 12 | const mach_compat = focus.mach_compat; 13 | 14 | pub const FileOpener = struct { 15 | app: *App, 16 | preview_editor: *Editor, 17 | input: SingleLineEditor, 18 | selector: Selector, 19 | 20 | pub fn init(app: *App, init_path: []const u8) *FileOpener { 21 | const empty_buffer = Buffer.initEmpty(app, .{ 22 | .limit_load_bytes = true, 23 | .enable_completions = false, 24 | .enable_undo = false, 25 | }); 26 | const preview_editor = Editor.init(app, empty_buffer, .{ 27 | .show_status_bar = false, 28 | .show_completer = false, 29 | }); 30 | const input = SingleLineEditor.init(app, init_path); 31 | const selector = Selector.init(app); 32 | const self = app.allocator.create(FileOpener) catch u.oom(); 33 | self.* = FileOpener{ 34 | .app = app, 35 | .preview_editor = preview_editor, 36 | .input = input, 37 | .selector = selector, 38 | }; 39 | return self; 40 | } 41 | 42 | pub fn deinit(self: *FileOpener) void { 43 | self.selector.deinit(); 44 | self.input.deinit(); 45 | const buffer = self.preview_editor.buffer; 46 | self.preview_editor.deinit(); 47 | buffer.deinit(); 48 | self.app.allocator.destroy(self); 49 | } 50 | 51 | pub fn frame(self: *FileOpener, window: *Window, rect: u.Rect, events: []const mach_compat.Event) void { 52 | const layout = window.layoutSearcherWithPreview(rect); 53 | 54 | // no event handling 55 | 56 | // run input frame 57 | const input_changed = self.input.frame(window, layout.input, events); 58 | if (input_changed == .Changed) self.selector.selected = 0; 59 | 60 | // get and filter completions 61 | const results_or_err = u.fuzzy_search_paths(self.app.frame_allocator, self.input.getText()); 62 | const results = results_or_err catch &[_][]const u8{}; 63 | 64 | // run selector frame 65 | var action: Selector.Action = .None; 66 | if (results_or_err) |_| { 67 | self.selector.setItems(results); 68 | action = self.selector.frame(window, layout.selector, events); 69 | } else |results_err| { 70 | const error_text = u.format(self.app.frame_allocator, "Error opening directory: {}", .{results_err}); 71 | window.queueText(layout.selector, style.emphasisRed, error_text); 72 | } 73 | 74 | const path = self.input.getText(); 75 | const dirname = self.app.frame_allocator.dupe( 76 | u8, 77 | if (path.len > 0 and std.fs.path.isSep(path[path.len - 1])) 78 | path[0 .. path.len - 1] 79 | else 80 | std.fs.path.dirname(path) orelse "", 81 | ) catch u.oom(); 82 | 83 | // maybe open file 84 | if (action == .SelectRaw or action == .SelectOne) { 85 | const filename: []const u8 = if (action == .SelectRaw) 86 | self.app.frame_allocator.dupe(u8, self.input.getText()) catch u.oom() 87 | else 88 | std.fs.path.join(self.app.frame_allocator, &[_][]const u8{ dirname, results[self.selector.selected] }) catch u.oom(); 89 | if (filename.len > 0 and std.fs.path.isSep(filename[filename.len - 1])) { 90 | if (action == .SelectRaw) 91 | std.fs.cwd().makeDir(filename) catch |err| { 92 | u.panic("{} while creating directory {s}", .{ err, filename }); 93 | }; 94 | self.input.setText(filename); 95 | self.selector.selected = 0; 96 | } else { 97 | if (action == .SelectRaw) { 98 | const file = std.fs.cwd().createFile(filename, .{ .truncate = false }) catch |err| { 99 | u.panic("{} while creating file {s}", .{ err, filename }); 100 | }; 101 | file.close(); 102 | } 103 | const new_buffer = self.app.getBufferFromAbsoluteFilename(filename); 104 | const new_editor = Editor.init(self.app, new_buffer, .{}); 105 | window.popView(); 106 | window.pushView(new_editor); 107 | } 108 | } 109 | 110 | // update preview 111 | const buffer = self.preview_editor.buffer; 112 | self.preview_editor.deinit(); 113 | buffer.deinit(); 114 | if (self.selector.selected >= results.len) { 115 | const empty_buffer = Buffer.initEmpty(self.app, .{ 116 | .limit_load_bytes = true, 117 | .enable_completions = false, 118 | .enable_undo = false, 119 | }); 120 | self.preview_editor = Editor.init(self.app, empty_buffer, .{ 121 | .show_status_bar = false, 122 | .show_completer = false, 123 | }); 124 | } else { 125 | const selected = results[self.selector.selected]; 126 | if (std.mem.endsWith(u8, selected, "/")) { 127 | const empty_buffer = Buffer.initEmpty(self.app, .{ 128 | .limit_load_bytes = true, 129 | .enable_completions = false, 130 | .enable_undo = false, 131 | }); 132 | self.preview_editor = Editor.init(self.app, empty_buffer, .{ 133 | .show_status_bar = false, 134 | .show_completer = false, 135 | }); 136 | } else { 137 | const filename = std.fs.path.join(self.app.frame_allocator, &[_][]const u8{ dirname, selected }) catch u.oom(); 138 | 139 | const preview_buffer = Buffer.initFromAbsoluteFilename(self.app, .{ 140 | .limit_load_bytes = true, 141 | .enable_completions = false, 142 | .enable_undo = false, 143 | }, filename); 144 | self.preview_editor = Editor.init(self.app, preview_buffer, .{ 145 | .show_status_bar = false, 146 | .show_completer = false, 147 | }); 148 | } 149 | } 150 | 151 | // run preview frame 152 | self.preview_editor.frame(window, layout.preview, &[0]mach_compat.Event{}); 153 | } 154 | }; 155 | -------------------------------------------------------------------------------- /lib/focus/error_lister.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | const Editor = focus.Editor; 8 | const Window = focus.Window; 9 | const style = focus.style; 10 | const Selector = focus.Selector; 11 | const mach_compat = focus.mach_compat; 12 | 13 | pub const ErrorLister = struct { 14 | app: *App, 15 | error_source_editor: *Editor, 16 | error_report_buffer_init: *Buffer, 17 | error_report_editor: *Editor, 18 | selector: Selector, 19 | 20 | pub fn init(app: *App) *ErrorLister { 21 | const error_source_buffer = Buffer.initEmpty(app, .{ 22 | .enable_completions = false, 23 | .enable_undo = false, 24 | }); 25 | const error_source_editor = Editor.init(app, error_source_buffer, .{ 26 | .show_status_bar = false, 27 | .show_completer = false, 28 | }); 29 | 30 | const error_report_buffer = Buffer.initEmpty(app, .{ 31 | .enable_completions = false, 32 | .enable_undo = false, 33 | }); 34 | const error_report_editor = Editor.init(app, error_report_buffer, .{ 35 | .show_status_bar = false, 36 | .show_completer = false, 37 | }); 38 | 39 | var selector = Selector.init(app); 40 | selector.selected = app.last_error_lister_selected; 41 | const self = app.allocator.create(ErrorLister) catch u.oom(); 42 | self.* = ErrorLister{ 43 | .app = app, 44 | .selector = selector, 45 | .error_source_editor = error_source_editor, 46 | .error_report_buffer_init = error_report_buffer, 47 | .error_report_editor = error_report_editor, 48 | }; 49 | return self; 50 | } 51 | 52 | pub fn deinit(self: *ErrorLister) void { 53 | { 54 | // error_report might point to an buffer allocated elsewhere 55 | self.error_report_editor.deinit(); 56 | self.error_report_buffer_init.deinit(); 57 | } 58 | { 59 | // error_source always deinits it's old buffer when changing. 60 | const buffer = self.error_source_editor.buffer; 61 | self.error_source_editor.deinit(); 62 | buffer.deinit(); 63 | } 64 | self.selector.deinit(); 65 | self.app.allocator.destroy(self); 66 | } 67 | 68 | pub const ErrorLocation = struct { 69 | report_buffer: *Buffer, 70 | report_location: [2]usize, 71 | path: []const u8, 72 | line: usize, 73 | col: usize, 74 | 75 | pub fn deinit(self: ErrorLocation, allocator: u.Allocator) void { 76 | allocator.free(self.path); 77 | } 78 | }; 79 | 80 | pub fn frame(self: *ErrorLister, window: *Window, rect: u.Rect, events: []const mach_compat.Event) void { 81 | const layout = window.layoutLister(rect); 82 | 83 | // get error locations 84 | var error_locations = u.ArrayList(ErrorLocation).init(self.app.frame_allocator); 85 | for (self.app.windows.items) |other_window| { 86 | if (other_window.getTopView()) |view| { 87 | switch (view) { 88 | .Maker => |maker| switch (maker.state) { 89 | .Running => |running| error_locations.appendSlice(running.error_locations.items) catch u.oom(), 90 | else => {}, 91 | }, 92 | else => {}, 93 | } 94 | } 95 | } 96 | 97 | // run selector logic only, no rendering 98 | const action = self.selector.logic(events, error_locations.items.len); 99 | 100 | // set cache selection 101 | self.app.last_error_lister_selected = self.selector.selected; 102 | 103 | if (self.selector.selected < error_locations.items.len) { 104 | const error_location = error_locations.items[self.selector.selected]; 105 | 106 | // maybe open file 107 | if (action == .SelectOne) { 108 | const new_buffer = self.app.getBufferFromAbsoluteFilename(error_location.path); 109 | const new_editor = Editor.init(self.app, new_buffer, .{}); 110 | const cursor = new_editor.getMainCursor(); 111 | new_editor.tryGoRealLine(cursor, error_location.line - 1); 112 | new_editor.goRealCol(cursor, error_location.col - 1); 113 | new_editor.setCenterAtPos(cursor.head.pos); 114 | window.popView(); 115 | window.pushView(new_editor); 116 | } 117 | 118 | // show report 119 | // (error_report_buffer_init will get cleaned up on self.deinit()) 120 | // (other buffers are the responsibility of error sources) 121 | self.error_report_editor.deinit(); 122 | self.error_report_editor = Editor.init(self.app, error_location.report_buffer, .{ 123 | .show_status_bar = false, 124 | .show_completer = false, 125 | }); 126 | { 127 | const cursor = self.error_report_editor.getMainCursor(); 128 | self.error_report_editor.goPos(cursor, error_location.report_location[0]); 129 | self.error_report_editor.setMark(); 130 | self.error_report_editor.goPos(cursor, error_location.report_location[1]); 131 | self.error_report_editor.setCenterAtPos(error_location.report_location[0]); 132 | self.error_report_editor.frame(window, layout.report, &.{}); 133 | } 134 | 135 | // show source 136 | const buffer = self.error_source_editor.buffer; 137 | self.error_source_editor.deinit(); 138 | buffer.deinit(); 139 | const error_source_buffer = Buffer.initFromAbsoluteFilename(self.app, .{ 140 | .enable_completions = false, 141 | .enable_undo = false, 142 | }, error_location.path); 143 | self.error_source_editor = Editor.init(self.app, error_source_buffer, .{ 144 | .show_status_bar = false, 145 | .show_completer = false, 146 | }); 147 | { 148 | const cursor = self.error_source_editor.getMainCursor(); 149 | self.error_source_editor.tryGoRealLine(cursor, error_location.line - 1); 150 | self.error_source_editor.goRealCol(cursor, error_location.col - 1); 151 | self.error_source_editor.setCenterAtPos(cursor.head.pos); 152 | } 153 | self.error_source_editor.frame(window, layout.preview, &.{}); 154 | } 155 | } 156 | }; 157 | -------------------------------------------------------------------------------- /lib/focus/atlas.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const freetype = @import("mach-freetype"); 4 | const u = focus.util; 5 | const c = focus.util.c; 6 | 7 | const fira_code = @embedFile("../../fonts/Fira_Code_v5.2/woff/FiraCode-Regular.woff"); 8 | 9 | pub const Atlas = struct { 10 | allocator: u.Allocator, 11 | char_size_pixels: usize, 12 | texture: []u.Color, 13 | texture_dims: u.Vec2, 14 | char_width: u.Coord, 15 | char_height: u.Coord, 16 | char_to_rect: []u.Rect, 17 | white_rect: u.Rect, 18 | 19 | pub fn init(allocator: u.Allocator, char_size_pixels: usize) Atlas { 20 | // init freetype 21 | const lib = freetype.Library.init() catch |err| 22 | u.panic("Error initializing freetype: {}", .{err}); 23 | defer lib.deinit(); 24 | 25 | // load font 26 | const face = lib.createFaceMemory(fira_code, 0) catch |err| 27 | u.panic("Error loading font: {}", .{err}); 28 | defer face.deinit(); 29 | 30 | // set font size 31 | face.setPixelSizes(@intCast(char_size_pixels), @intCast(char_size_pixels)) catch |err| 32 | u.panic("Error setting font size: {}", .{err}); 33 | 34 | // render every ascii char 35 | const num_ascii_chars: usize = 128; 36 | const char_to_bitmap = allocator.alloc([]const u.Color, num_ascii_chars) catch u.oom(); 37 | const char_to_cbox = allocator.alloc(freetype.BBox, num_ascii_chars) catch u.oom(); 38 | { 39 | var char: usize = 0; 40 | while (char < num_ascii_chars) : (char += 1) { 41 | face.loadChar(@intCast(char), .{ .render = true }) catch |err| 42 | u.panic("Error loading '{}': {}", .{ char, err }); 43 | 44 | const bitmap = face.glyph().bitmap(); 45 | const bitmap_width = bitmap.width(); 46 | const bitmap_height = bitmap.rows(); 47 | const bitmap_pitch = bitmap.pitch(); 48 | const bitmap_copy = allocator.alloc(u.Color, bitmap_width * bitmap_height) catch u.oom(); 49 | if (bitmap.buffer()) |buffer| { 50 | var x: usize = 0; 51 | while (x < bitmap_width) : (x += 1) { 52 | var y: usize = 0; 53 | while (y < bitmap_height) : (y += 1) { 54 | bitmap_copy[(y * bitmap_width) + x] = .{ 55 | .r = 255, 56 | .g = 255, 57 | .b = 255, 58 | .a = buffer[(y * @as(usize, @intCast(bitmap_pitch))) + x], 59 | }; 60 | } 61 | } 62 | } 63 | 64 | const glyph = face.glyph().getGlyph() catch |err| 65 | u.panic("Error getting glyph for '{}': {}", .{ char, err }); 66 | const cbox = glyph.getCBox(.pixels); 67 | u.assert(cbox.yMax == face.glyph().bitmapTop()); 68 | u.assert(cbox.yMin == face.glyph().bitmapTop() - @as(i32, @intCast(bitmap.rows()))); 69 | u.assert(cbox.xMin == face.glyph().bitmapLeft()); 70 | u.assert(cbox.xMax == face.glyph().bitmapLeft() + @as(i32, @intCast(bitmap.width()))); 71 | 72 | char_to_bitmap[char] = bitmap_copy; 73 | char_to_cbox[char] = cbox; 74 | } 75 | } 76 | defer { 77 | for (char_to_bitmap) |bitmap| allocator.free(bitmap); 78 | allocator.free(char_to_bitmap); 79 | allocator.free(char_to_cbox); 80 | } 81 | 82 | // figure out how much space we need per character 83 | var max_cbox = freetype.BBox{ .xMin = 0, .yMin = 0, .xMax = 0, .yMax = 0 }; 84 | for (char_to_cbox) |cbox| { 85 | max_cbox.xMin = @min(max_cbox.xMin, cbox.xMin); 86 | max_cbox.yMin = @min(max_cbox.yMin, cbox.yMin); 87 | max_cbox.xMax = @max(max_cbox.xMax, cbox.xMax); 88 | max_cbox.yMax = @max(max_cbox.yMax, cbox.yMax); 89 | } 90 | const char_width = @as(u.Coord, @intCast(max_cbox.xMax - max_cbox.xMin)); 91 | const char_height = @as(u.Coord, @intCast(max_cbox.yMax - max_cbox.yMin)); 92 | 93 | // copy every char into a single texture 94 | const num_chars = num_ascii_chars + 1; // ascii + one white box 95 | const texture = allocator.alloc(u.Color, num_chars * @as(usize, @intCast(char_width * char_height))) catch u.oom(); 96 | for (texture) |*pixel| pixel.* = .{ .r = 0, .g = 0, .b = 0, .a = 0 }; 97 | const char_to_rect = allocator.alloc(u.Rect, num_chars) catch u.oom(); 98 | for (char_to_bitmap, 0..) |bitmap, char| { 99 | const cbox = char_to_cbox[char]; 100 | 101 | const bitmap_width = cbox.xMax - cbox.xMin; 102 | const bitmap_height = cbox.yMax - cbox.yMin; 103 | 104 | var by: i32 = 0; 105 | while (by < bitmap_height) : (by += 1) { 106 | var bx: i32 = 0; 107 | while (bx < bitmap_width) : (bx += 1) { 108 | const tx = (@as(u.Coord, @intCast(char)) * char_width) + bx + (cbox.xMin - max_cbox.xMin); 109 | const ty = by + (max_cbox.yMax - cbox.yMax); 110 | const ti = @as(usize, @intCast((ty * @as(u.Coord, @intCast(num_chars)) * char_width) + tx)); 111 | const bi = @as(usize, @intCast((by * bitmap_width) + bx)); 112 | texture[ti] = bitmap[bi]; 113 | } 114 | } 115 | 116 | char_to_rect[char] = .{ 117 | .x = @as(u.Coord, @intCast(char)) * char_width, 118 | .y = 0, 119 | .w = char_width, 120 | .h = char_height, 121 | }; 122 | } 123 | 124 | // make a white pixel 125 | const white_rect = u.Rect{ .x = @as(u.Coord, @intCast(num_ascii_chars)) * char_width, .y = 0, .w = 1, .h = 1 }; 126 | texture[@intCast(white_rect.x)] = u.Color{ .r = 255, .g = 255, .b = 255, .a = 255 }; 127 | 128 | return Atlas{ 129 | .allocator = allocator, 130 | .char_size_pixels = char_size_pixels, 131 | .texture = texture, 132 | .texture_dims = .{ 133 | .x = @as(u.Coord, @intCast(num_chars)) * char_width, 134 | .y = char_height, 135 | }, 136 | .char_width = @intCast(char_width), 137 | .char_height = @intCast(char_height), 138 | .char_to_rect = char_to_rect, 139 | .white_rect = white_rect, 140 | }; 141 | } 142 | 143 | pub fn deinit(self: *Atlas) void { 144 | self.allocator.free(self.char_to_rect); 145 | self.allocator.free(self.texture); 146 | } 147 | }; 148 | -------------------------------------------------------------------------------- /lib/focus/buffer_searcher.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | const Editor = focus.Editor; 8 | const SingleLineEditor = focus.SingleLineEditor; 9 | const Selector = focus.Selector; 10 | const Window = focus.Window; 11 | const style = focus.style; 12 | const mach_compat = focus.mach_compat; 13 | 14 | pub const BufferSearcher = struct { 15 | app: *App, 16 | target_editor: *Editor, 17 | preview_editor: *Editor, 18 | input: SingleLineEditor, 19 | selector: Selector, 20 | // The position of the currently selected item, or the start pos if nothing was selected yet. 21 | // This just helps preserve position in the editor when jumping in and out of the buffer searcher. 22 | selection_pos: usize, 23 | init_selection_pos: usize, 24 | 25 | pub fn init(app: *App, target_editor: *Editor) *BufferSearcher { 26 | const preview_editor = Editor.init(app, target_editor.buffer, .{ 27 | .show_status_bar = false, 28 | .show_completer = false, 29 | }); 30 | preview_editor.getMainCursor().* = target_editor.getMainCursor().*; 31 | const input = SingleLineEditor.init(app, app.last_search_filter); 32 | input.editor.goRealLineStart(input.editor.getMainCursor()); 33 | input.editor.setMark(); 34 | input.editor.goRealLineEnd(input.editor.getMainCursor()); 35 | const selector = Selector.init(app); 36 | const selection_pos = target_editor.getMainCursor().head.pos; 37 | const self = app.allocator.create(BufferSearcher) catch u.oom(); 38 | self.* = BufferSearcher{ 39 | .app = app, 40 | .target_editor = target_editor, 41 | .preview_editor = preview_editor, 42 | .input = input, 43 | .selector = selector, 44 | .selection_pos = selection_pos, 45 | .init_selection_pos = selection_pos, 46 | }; 47 | return self; 48 | } 49 | 50 | pub fn deinit(self: *BufferSearcher) void { 51 | self.selector.deinit(); 52 | self.input.deinit(); 53 | self.preview_editor.deinit(); 54 | // dont own target_editor 55 | self.app.allocator.destroy(self); 56 | } 57 | 58 | pub fn frame(self: *BufferSearcher, window: *Window, rect: u.Rect, events: []const mach_compat.Event) void { 59 | const layout = window.layoutSearcherWithPreview(rect); 60 | 61 | // run input frame 62 | const input_changed = self.input.frame(window, layout.input, events); 63 | if (input_changed == .Changed) self.selection_pos = self.init_selection_pos; 64 | 65 | // search buffer 66 | const filter = self.input.getText(); 67 | var results = u.ArrayList([]const u8).init(self.app.frame_allocator); 68 | var result_poss = u.ArrayList(usize).init(self.app.frame_allocator); 69 | { 70 | const max_line_string = u.format(self.app.frame_allocator, "{}", .{self.preview_editor.buffer.countLines()}); 71 | if (filter.len > 0) { 72 | var pos: usize = 0; 73 | var i: usize = 0; 74 | while (self.preview_editor.buffer.searchForwards(pos, filter)) |found_pos| { 75 | const start = self.preview_editor.buffer.getLineStart(found_pos); 76 | const end = self.preview_editor.buffer.getLineEnd(found_pos + filter.len); 77 | const selection = self.preview_editor.buffer.dupe(self.app.frame_allocator, start, end); 78 | u.assert(selection[0] != '\n' and selection[selection.len - 1] != '\n'); 79 | 80 | var result = u.ArrayList(u8).init(self.app.frame_allocator); 81 | const line = self.preview_editor.buffer.getLineColForPos(found_pos)[0]; 82 | const line_string = u.format(self.app.frame_allocator, "{}", .{line}); 83 | result.appendSlice(line_string) catch u.oom(); 84 | result.appendNTimes(' ', max_line_string.len - line_string.len + 1) catch u.oom(); 85 | result.appendSlice(selection) catch u.oom(); 86 | 87 | results.append(result.toOwnedSlice() catch u.oom()) catch u.oom(); 88 | result_poss.append(found_pos) catch u.oom(); 89 | 90 | pos = found_pos + filter.len; 91 | i += 1; 92 | } 93 | } 94 | } 95 | 96 | // update selection 97 | self.selector.selected = @max(result_poss.items.len, 1) - 1; 98 | for (result_poss.items, 0..) |result_pos, i| { 99 | if (self.selection_pos < result_pos + filter.len) { 100 | self.selector.selected = i; 101 | break; 102 | } 103 | } 104 | 105 | // run selector frame 106 | const old_selected = self.selector.selected; 107 | self.selector.setItems(results.items); 108 | const action = self.selector.frame(window, layout.selector, events); 109 | if (self.selector.selected != old_selected and self.selector.selected < result_poss.items.len) { 110 | self.selection_pos = result_poss.items[self.selector.selected]; 111 | } 112 | switch (action) { 113 | .None, .SelectRaw => {}, 114 | .SelectOne, .SelectAll => { 115 | self.updateEditor(self.target_editor, action, result_poss.items, filter); 116 | self.target_editor.top_pixel = self.preview_editor.top_pixel; 117 | window.popView(); 118 | }, 119 | } 120 | 121 | // set cached search text 122 | self.app.allocator.free(self.app.last_search_filter); 123 | self.app.last_search_filter = self.app.dupe(self.input.getText()); 124 | 125 | // update preview 126 | self.updateEditor(self.preview_editor, action, result_poss.items, filter); 127 | 128 | // run preview frame 129 | self.preview_editor.frame(window, layout.preview, &[0]mach_compat.Event{}); 130 | } 131 | 132 | fn updateEditor(self: *BufferSearcher, editor: *Editor, action: Selector.Action, result_poss: []usize, filter: []const u8) void { 133 | // TODO centre view on main cursor 134 | editor.collapseCursors(); 135 | editor.setMark(); 136 | switch (action) { 137 | .None, .SelectRaw, .SelectOne => { 138 | if (self.selector.selected < result_poss.len) { 139 | const pos = result_poss[self.selector.selected]; 140 | var cursor = editor.getMainCursor(); 141 | editor.goPos(cursor, pos + filter.len); 142 | editor.updatePos(&cursor.tail, pos); 143 | editor.setCenterAtPos(pos); 144 | } 145 | }, 146 | .SelectAll => { 147 | for (result_poss, 0..) |pos, i| { 148 | var cursor = if (i == 0) editor.getMainCursor() else editor.addCursor(); 149 | editor.goPos(cursor, pos + filter.len); 150 | editor.updatePos(&cursor.tail, pos); 151 | editor.setCenterAtPos(pos); 152 | } 153 | }, 154 | } 155 | } 156 | }; 157 | -------------------------------------------------------------------------------- /lib/focus/project_file_opener.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | const Editor = focus.Editor; 8 | const SingleLineEditor = focus.SingleLineEditor; 9 | const Window = focus.Window; 10 | const style = focus.style; 11 | const Selector = focus.Selector; 12 | const mach_compat = focus.mach_compat; 13 | 14 | pub const ProjectFileOpener = struct { 15 | app: *App, 16 | preview_editor: *Editor, 17 | input: SingleLineEditor, 18 | selector: Selector, 19 | paths: []const []const u8, 20 | 21 | pub fn init(app: *App) *ProjectFileOpener { 22 | const empty_buffer = Buffer.initEmpty(app, .{ 23 | .limit_load_bytes = true, 24 | .enable_completions = false, 25 | .enable_undo = false, 26 | }); 27 | const preview_editor = Editor.init(app, empty_buffer, .{ 28 | .show_status_bar = false, 29 | .show_completer = false, 30 | }); 31 | const input = SingleLineEditor.init(app, app.last_file_filter); 32 | input.editor.goRealLineStart(input.editor.getMainCursor()); 33 | input.editor.setMark(); 34 | input.editor.goRealLineEnd(input.editor.getMainCursor()); 35 | var selector = Selector.init(app); 36 | selector.selected = app.last_project_file_opener_selected; 37 | 38 | var projects = u.ArrayList(u8).init(app.frame_allocator); 39 | { 40 | const file = std.fs.cwd().openFile(focus.config.projects_file_path, .{}) catch |err| 41 | u.panic("{} while opening {s}", .{ err, focus.config.projects_file_path }); 42 | defer file.close(); 43 | file.reader().readAllArrayList(&projects, std.math.maxInt(usize)) catch |err| 44 | u.panic("{} while reading {s}", .{ err, focus.config.projects_file_path }); 45 | } 46 | 47 | var paths = u.ArrayList([]const u8).init(app.allocator); 48 | var projects_iter = std.mem.splitScalar(u8, projects.items, '\n'); 49 | while (projects_iter.next()) |project_untrimmed| { 50 | const project = std.mem.trim(u8, project_untrimmed, " "); 51 | if (project.len == 0) continue; 52 | if (std.mem.startsWith(u8, project, "#")) continue; 53 | const result = std.process.Child.run(.{ 54 | .allocator = app.frame_allocator, 55 | .argv = &[_][]const u8{ "rg", "--files", "-0" }, 56 | .cwd = project, 57 | .max_output_bytes = 128 * 1024 * 1024, 58 | }) catch |err| u.panic("{} while calling rg", .{err}); 59 | u.assert(result.term == .Exited and result.term.Exited == 0); 60 | var lines = std.mem.splitScalar(u8, result.stdout, 0); 61 | while (lines.next()) |line| { 62 | const path = std.fs.path.join(app.allocator, &[2][]const u8{ project, line }) catch u.oom(); 63 | paths.append(path) catch u.oom(); 64 | } 65 | } 66 | 67 | std.mem.sort([]const u8, paths.items, {}, struct { 68 | fn lessThan(_: void, a: []const u8, b: []const u8) bool { 69 | return std.mem.lessThan(u8, a, b); 70 | } 71 | }.lessThan); 72 | 73 | const self = app.allocator.create(ProjectFileOpener) catch u.oom(); 74 | self.* = ProjectFileOpener{ 75 | .app = app, 76 | .preview_editor = preview_editor, 77 | .input = input, 78 | .selector = selector, 79 | .paths = paths.toOwnedSlice() catch u.oom(), 80 | }; 81 | return self; 82 | } 83 | 84 | pub fn deinit(self: *ProjectFileOpener) void { 85 | for (self.paths) |completion| { 86 | self.app.allocator.free(completion); 87 | } 88 | self.app.allocator.free(self.paths); 89 | self.selector.deinit(); 90 | self.input.deinit(); 91 | const buffer = self.preview_editor.buffer; 92 | self.preview_editor.deinit(); 93 | buffer.deinit(); 94 | self.app.allocator.destroy(self); 95 | } 96 | 97 | pub fn frame(self: *ProjectFileOpener, window: *Window, rect: u.Rect, events: []const mach_compat.Event) void { 98 | const layout = window.layoutSearcherWithPreview(rect); 99 | 100 | // run input frame 101 | const input_changed = self.input.frame(window, layout.input, events); 102 | if (input_changed == .Changed) self.selector.selected = 0; 103 | 104 | // filter paths 105 | const filtered_paths = u.fuzzy_search(self.app.frame_allocator, self.paths, self.input.getText()); 106 | 107 | // run selector frame 108 | self.selector.setItems(filtered_paths); 109 | const action = self.selector.frame(window, layout.selector, events); 110 | 111 | // maybe open file 112 | if (action == .SelectOne) { 113 | const path = filtered_paths[self.selector.selected]; 114 | if (path.len > 0 and std.fs.path.isSep(path[path.len - 1])) { 115 | self.input.setText(path); 116 | } else { 117 | const new_buffer = self.app.getBufferFromAbsoluteFilename(path); 118 | const new_editor = Editor.init(self.app, new_buffer, .{}); 119 | window.popView(); 120 | window.pushView(new_editor); 121 | } 122 | } 123 | 124 | // set cached search text 125 | self.app.allocator.free(self.app.last_file_filter); 126 | self.app.last_file_filter = self.app.dupe(self.input.getText()); 127 | self.app.last_project_file_opener_selected = self.selector.selected; 128 | 129 | // update preview 130 | const buffer = self.preview_editor.buffer; 131 | self.preview_editor.deinit(); 132 | buffer.deinit(); 133 | if (self.selector.selected >= filtered_paths.len) { 134 | const empty_buffer = Buffer.initEmpty(self.app, .{ 135 | .limit_load_bytes = true, 136 | .enable_completions = false, 137 | .enable_undo = false, 138 | }); 139 | self.preview_editor = Editor.init(self.app, empty_buffer, .{ 140 | .show_status_bar = false, 141 | .show_completer = false, 142 | }); 143 | } else { 144 | const selected = filtered_paths[self.selector.selected]; 145 | if (std.mem.endsWith(u8, selected, "/")) { 146 | const empty_buffer = Buffer.initEmpty(self.app, .{ 147 | .limit_load_bytes = true, 148 | .enable_completions = false, 149 | .enable_undo = false, 150 | }); 151 | self.preview_editor = Editor.init(self.app, empty_buffer, .{ 152 | .show_status_bar = false, 153 | .show_completer = false, 154 | }); 155 | } else { 156 | const preview_buffer = Buffer.initFromAbsoluteFilename(self.app, .{ 157 | .limit_load_bytes = true, 158 | .enable_completions = false, 159 | .enable_undo = false, 160 | }, selected); 161 | self.preview_editor = Editor.init(self.app, preview_buffer, .{ 162 | .show_status_bar = false, 163 | .show_completer = false, 164 | }); 165 | } 166 | } 167 | 168 | // run preview frame 169 | self.preview_editor.frame(window, layout.preview, &[0]mach_compat.Event{}); 170 | } 171 | }; 172 | -------------------------------------------------------------------------------- /lib/focus/project_searcher.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | const Editor = focus.Editor; 8 | const SingleLineEditor = focus.SingleLineEditor; 9 | const Window = focus.Window; 10 | const style = focus.style; 11 | const Selector = focus.Selector; 12 | const ChildProcess = focus.ChildProcess; 13 | const mach_compat = focus.mach_compat; 14 | 15 | pub const ProjectSearcher = struct { 16 | app: *App, 17 | project_dir: []const u8, 18 | preview_editor: *Editor, 19 | filter_mode: FilterMode, 20 | input: SingleLineEditor, 21 | selector: Selector, 22 | child_process: ?ChildProcess, 23 | 24 | const FilterMode = enum { 25 | FixedStrings, 26 | Regexp, 27 | }; 28 | 29 | pub fn init(app: *App, project_dir: []const u8, init_filter_mode: FilterMode, init_filter: ?[]const u8) *ProjectSearcher { 30 | const empty_buffer = Buffer.initEmpty(app, .{ 31 | .enable_completions = false, 32 | .enable_undo = false, 33 | }); 34 | const preview_editor = Editor.init(app, empty_buffer, .{ 35 | .show_status_bar = false, 36 | .show_completer = false, 37 | }); 38 | const input = SingleLineEditor.init(app, init_filter orelse app.last_search_filter); 39 | input.editor.goRealLineStart(input.editor.getMainCursor()); 40 | input.editor.setMark(); 41 | input.editor.goRealLineEnd(input.editor.getMainCursor()); 42 | var selector = Selector.init(app); 43 | selector.selected = app.last_project_search_selected; 44 | 45 | const self = app.allocator.create(ProjectSearcher) catch u.oom(); 46 | self.* = 47 | ProjectSearcher{ 48 | .app = app, 49 | .project_dir = project_dir, 50 | .preview_editor = preview_editor, 51 | .filter_mode = init_filter_mode, 52 | .input = input, 53 | .selector = selector, 54 | .child_process = null, 55 | }; 56 | self.startRipgrep(); 57 | return self; 58 | } 59 | 60 | pub fn deinit(self: *ProjectSearcher) void { 61 | if (self.child_process) |*child_process| child_process.deinit(); 62 | self.selector.deinit(); 63 | self.input.deinit(); 64 | const buffer = self.preview_editor.buffer; 65 | self.preview_editor.deinit(); 66 | buffer.deinit(); 67 | // TODO should self own project_dir? 68 | self.app.allocator.destroy(self); 69 | } 70 | 71 | fn startRipgrep(self: *ProjectSearcher) void { 72 | const filter = self.input.getText(); 73 | if (filter.len > 0) { 74 | if (self.child_process) |*child_process| child_process.deinit(); 75 | self.child_process = ChildProcess.init( 76 | self.app.allocator, 77 | self.project_dir, 78 | switch (self.filter_mode) { 79 | .FixedStrings => &[_][]const u8{ 80 | "rg", 81 | "--line-number", 82 | "--sort", 83 | "path", 84 | "--fixed-strings", 85 | "--", 86 | filter, 87 | }, 88 | .Regexp => &[_][]const u8{ 89 | "rg", 90 | "--line-number", 91 | "--sort", 92 | "path", 93 | "--regexp", 94 | filter, 95 | }, 96 | }, 97 | ); 98 | } 99 | } 100 | 101 | pub fn frame(self: *ProjectSearcher, window: *Window, rect: u.Rect, events: []const mach_compat.Event) void { 102 | const layout = window.layoutSearcherWithPreview(rect); 103 | 104 | // run input frame 105 | const input_changed = self.input.frame(window, layout.input, events); 106 | 107 | // maybe start ripgrep 108 | if (input_changed == .Changed) { 109 | self.selector.selected = 0; 110 | self.selector.setTextAndRanges("", &.{}); 111 | if (self.child_process) |*child_process| { 112 | child_process.deinit(); 113 | self.child_process = null; 114 | } 115 | self.startRipgrep(); 116 | } 117 | 118 | // if output changed, update selector ranges 119 | if (self.child_process) |child_process| { 120 | const new_text = child_process.read(self.app.frame_allocator); 121 | if (new_text.len > 0) { 122 | self.selector.buffer.insert(self.selector.buffer.getBufferEnd(), new_text); 123 | const text = self.selector.buffer.bytes.items; 124 | var result_ranges = u.ArrayList([2]usize).init(self.app.frame_allocator); 125 | var start: usize = 0; 126 | var end: usize = 0; 127 | while (end < text.len) { 128 | end = std.mem.indexOfPos(u8, text, start, "\n") orelse text.len; 129 | if (start != end) 130 | result_ranges.append(.{ start, end }) catch u.oom(); 131 | start = end + 1; 132 | } 133 | self.selector.setRanges(result_ranges.toOwnedSlice() catch u.oom()); 134 | } 135 | } 136 | 137 | // run selector frame 138 | const action = self.selector.frame(window, layout.selector, events); 139 | 140 | // set cached search text 141 | self.app.allocator.free(self.app.last_search_filter); 142 | self.app.last_search_filter = self.app.dupe(self.input.getText()); 143 | self.app.last_project_search_selected = self.selector.selected; 144 | 145 | var line_number: ?usize = null; 146 | var path: ?[]const u8 = null; 147 | if (self.selector.selected < self.selector.ranges.len) { 148 | // deinit old preview 149 | const buffer = self.preview_editor.buffer; 150 | self.preview_editor.deinit(); 151 | buffer.deinit(); 152 | 153 | // see if we can parse selection 154 | const range = self.selector.ranges[self.selector.selected]; 155 | const line = self.selector.buffer.bytes.items[range[0]..range[1]]; 156 | var parts = std.mem.splitScalar(u8, line, ':'); 157 | if (parts.next()) |path_suffix| 158 | path = std.fs.path.join(self.app.frame_allocator, &[2][]const u8{ self.project_dir, path_suffix }) catch u.oom(); 159 | if (parts.next()) |line_number_string| 160 | line_number = std.fmt.parseInt(usize, line_number_string, 10) catch null; 161 | 162 | // init new preview 163 | const preview_buffer = if (path != null) 164 | Buffer.initFromAbsoluteFilename(self.app, .{ 165 | .enable_completions = false, 166 | .enable_undo = false, 167 | }, path.?) 168 | else 169 | Buffer.initEmpty(self.app, .{ 170 | .enable_completions = false, 171 | .enable_undo = false, 172 | }); 173 | self.preview_editor = Editor.init(self.app, preview_buffer, .{ 174 | .show_status_bar = false, 175 | .show_completer = false, 176 | }); 177 | if (line_number != null) { 178 | const cursor = self.preview_editor.getMainCursor(); 179 | self.preview_editor.tryGoRealLine(cursor, line_number.? - 1); 180 | self.preview_editor.setMark(); 181 | self.preview_editor.goRealLineEnd(cursor); 182 | self.preview_editor.setCenterAtPos(cursor.head.pos); 183 | } 184 | 185 | // handle action 186 | if (action == .SelectOne and path != null and line_number != null) { 187 | const new_buffer = self.app.getBufferFromAbsoluteFilename(path.?); 188 | const new_editor = Editor.init(self.app, new_buffer, .{}); 189 | new_editor.top_pixel = self.preview_editor.top_pixel; 190 | const cursor = new_editor.getMainCursor(); 191 | new_editor.tryGoRealLine(cursor, line_number.? - 1); 192 | new_editor.setMark(); 193 | new_editor.goRealLineEnd(cursor); 194 | new_editor.setCenterAtPos(cursor.head.pos); 195 | window.popView(); 196 | window.pushView(new_editor); 197 | } 198 | } 199 | 200 | // run preview frame 201 | self.preview_editor.frame(window, layout.preview, &[0]mach_compat.Event{}); 202 | } 203 | }; 204 | -------------------------------------------------------------------------------- /lib/focus/language/zig.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const style = focus.style; 6 | const Language = focus.Language; 7 | 8 | pub const State = struct { 9 | allocator: u.Allocator, 10 | tokens: u.ArrayList(std.zig.Token.Tag), 11 | token_ranges: u.ArrayList([2]usize), 12 | paren_levels: u.ArrayList(usize), 13 | paren_parents: u.ArrayList(?usize), 14 | paren_matches: u.ArrayList(?usize), 15 | paren_match_stack: u.ArrayList(usize), 16 | mode: enum { 17 | Normal, 18 | NoStructure, 19 | Parens, 20 | }, 21 | squigglies: u.ArrayList(Language.Squiggly), 22 | 23 | pub fn init(allocator: u.Allocator, source: []const u8) State { 24 | var state: State = .{ 25 | .allocator = allocator, 26 | .tokens = u.ArrayList(std.zig.Token.Tag).init(allocator), 27 | .token_ranges = u.ArrayList([2]usize).init(allocator), 28 | .paren_levels = u.ArrayList(usize).init(allocator), 29 | .paren_parents = u.ArrayList(?usize).init(allocator), 30 | .paren_matches = u.ArrayList(?usize).init(allocator), 31 | .paren_match_stack = u.ArrayList(usize).init(allocator), 32 | .mode = .NoStructure, 33 | .squigglies = u.ArrayList(Language.Squiggly).init(allocator), 34 | }; 35 | state.reset(source); 36 | return state; 37 | } 38 | 39 | pub fn deinit(self: *State) void { 40 | self.squigglies.deinit(); 41 | self.paren_matches.deinit(); 42 | self.paren_parents.deinit(); 43 | self.paren_levels.deinit(); 44 | self.token_ranges.deinit(); 45 | self.tokens.deinit(); 46 | self.* = undefined; 47 | } 48 | 49 | fn reset(self: *State, source: []const u8) void { 50 | self.squigglies.shrinkRetainingCapacity(0); 51 | self.paren_match_stack.shrinkRetainingCapacity(0); 52 | self.paren_matches.shrinkRetainingCapacity(0); 53 | self.paren_parents.shrinkRetainingCapacity(0); 54 | self.paren_levels.shrinkRetainingCapacity(0); 55 | self.token_ranges.shrinkRetainingCapacity(0); 56 | self.tokens.shrinkRetainingCapacity(0); 57 | 58 | // TODO Can we avoid this allocation? 59 | const source_z = self.allocator.dupeZ(u8, source) catch u.oom(); 60 | defer self.allocator.free(source_z); 61 | 62 | var tokenizer = std.zig.Tokenizer.init(source_z); 63 | while (true) { 64 | const token = tokenizer.next(); 65 | if (token.tag == .eof) break; 66 | self.tokens.append(token.tag) catch u.oom(); 67 | self.token_ranges.append(.{ token.loc.start, token.loc.end }) catch u.oom(); 68 | } 69 | 70 | self.paren_levels.appendNTimes(0, self.tokens.items.len) catch u.oom(); 71 | self.paren_parents.appendNTimes(null, self.tokens.items.len) catch u.oom(); 72 | self.paren_matches.appendNTimes(null, self.tokens.items.len) catch u.oom(); 73 | 74 | for (self.tokens.items, 0..) |token, ix| { 75 | switch (token) { 76 | .r_paren, .r_brace, .r_bracket => { 77 | if (self.paren_match_stack.pop()) |matching_ix| { 78 | self.paren_matches.items[ix] = matching_ix; 79 | self.paren_matches.items[matching_ix] = ix; 80 | } 81 | }, 82 | else => {}, 83 | } 84 | if (self.paren_match_stack.items.len > 0) 85 | self.paren_parents.items[ix] = self.paren_match_stack.items[self.paren_match_stack.items.len - 1]; 86 | self.paren_levels.items[ix] = self.paren_match_stack.items.len; 87 | switch (token) { 88 | .l_paren, .l_brace, .l_bracket => { 89 | self.paren_match_stack.append(ix) catch u.oom(); 90 | }, 91 | else => {}, 92 | } 93 | } 94 | 95 | // Disabled long line warning. 96 | // 97 | //{ 98 | // var line_start: usize = 0; 99 | // while (line_start < source.len) { 100 | // var line_end = line_start; 101 | // while (line_end < source.len and source[line_end] != '\n') : (line_end += 1) {} 102 | // if (line_end - line_start >= 100) 103 | // squigglies.append(.{ 104 | // .color = style.emphasisOrange, 105 | // .range = .{ line_start + 99, line_end }, 106 | // }) catch u.oom(); 107 | // line_start = line_end + 1; 108 | // } 109 | //} 110 | } 111 | 112 | pub fn updateBeforeChange(self: *State, source: []const u8, delete_range: [2]usize) void { 113 | _ = self; 114 | _ = source; 115 | _ = delete_range; 116 | } 117 | 118 | pub fn updateAfterChange(self: *State, source: []const u8, insert_range: [2]usize) void { 119 | _ = insert_range; 120 | self.reset(source); 121 | } 122 | 123 | pub fn toggleMode(self: *State) void { 124 | self.mode = switch (self.mode) { 125 | .Normal => .NoStructure, 126 | .NoStructure => .Parens, 127 | .Parens => .Normal, 128 | }; 129 | } 130 | 131 | pub fn highlight(self: State, source: []const u8, range: [2]usize, colors: []u.Color) void { 132 | @memset(colors, style.comment_color); 133 | for (self.token_ranges.items, 0..) |token_range, i| { 134 | const source_start = token_range[0]; 135 | const source_end = token_range[1]; 136 | if (source_end < range[0] or source_start > range[1]) continue; 137 | const colors_start = if (source_start > range[0]) source_start - range[0] else 0; 138 | const colors_end = if (source_end > range[1]) range[1] - range[0] else source_end - range[0]; 139 | const token = self.tokens.items[i]; 140 | const structure_color = if (self.mode == .Normal) 141 | style.keyword_color 142 | else 143 | style.comment_color; 144 | const color = switch (token) { 145 | .doc_comment, .container_doc_comment => style.comment_color, 146 | .identifier, .builtin, .number_literal => if (self.mode == .Parens) style.comment_color else style.identColor(source[source_start..source_end]), 147 | .keyword_try, .keyword_catch, .keyword_error => if (self.mode == .Parens) style.comment_color else style.emphasisRed, 148 | .keyword_defer, .keyword_errdefer => if (self.mode == .Parens) style.comment_color else style.emphasisOrange, 149 | .keyword_break, .keyword_continue, .keyword_return => if (self.mode == .Parens) style.comment_color else style.emphasisGreen, 150 | .l_paren, .l_brace, .l_bracket, .r_paren, .r_brace, .r_bracket => color: { 151 | var is_good_match = false; 152 | if (self.paren_matches.items[i]) |matching_ix| { 153 | const matching_token = self.tokens.items[matching_ix]; 154 | is_good_match = switch (token) { 155 | .l_paren => matching_token == .r_paren, 156 | .l_brace => matching_token == .r_brace, 157 | .l_bracket => matching_token == .r_bracket, 158 | .r_paren => matching_token == .l_paren, 159 | .r_brace => matching_token == .l_brace, 160 | .r_bracket => matching_token == .l_bracket, 161 | else => unreachable, 162 | }; 163 | } 164 | break :color if (is_good_match) 165 | if (self.mode == .Parens) 166 | style.parenColor(self.paren_levels.items[i]) 167 | else 168 | structure_color 169 | else 170 | style.emphasisRed; 171 | }, 172 | .pipe, .equal_angle_bracket_right, .comma, .semicolon, .colon, .keyword_const, .keyword_pub => structure_color, 173 | else => if (self.mode == .Parens) style.comment_color else style.keyword_color, 174 | }; 175 | @memset(colors[colors_start..colors_end], color); 176 | } 177 | } 178 | 179 | pub fn format(self: State, frame_allocator: u.Allocator, source: []const u8) ?[]const u8 { 180 | // TODO Once zig syntax is stable, use Ast.render instead of shelling out 181 | 182 | var child_process = std.process.Child.init( 183 | &[_][]const u8{ "setsid", "zig", "fmt", "--stdin" }, 184 | self.allocator, 185 | ); 186 | 187 | child_process.stdin_behavior = .Pipe; 188 | child_process.stdout_behavior = .Pipe; 189 | child_process.stderr_behavior = .Pipe; 190 | 191 | child_process.spawn() catch |err| 192 | u.panic("Error spawning `zig fmt`: {}", .{err}); 193 | 194 | child_process.stdin.?.writeAll(source) catch |err| 195 | u.panic("Error writing to `zig fmt` stdin: {}", .{err}); 196 | child_process.stdin.?.close(); 197 | child_process.stdin = null; 198 | 199 | var stdout = std.ArrayListUnmanaged(u8).empty; 200 | var stderr = std.ArrayListUnmanaged(u8).empty; 201 | 202 | child_process.collectOutput(frame_allocator, &stdout, &stderr, std.math.maxInt(usize)) catch |err| 203 | u.panic("Error collecting output from `zig fmt`: {}", .{err}); 204 | 205 | const result = child_process.wait() catch |err| 206 | u.panic("Error waiting for `zig fmt`: {}", .{err}); 207 | 208 | if (u.deepEqual(result, .{ .Exited = 0 })) { 209 | return stdout.toOwnedSlice(frame_allocator) catch u.oom(); 210 | } else { 211 | u.warn("`zig fmt` failed: {s}", .{stderr.items}); 212 | return null; 213 | } 214 | } 215 | 216 | pub fn getAddedIndent(self: State, token_ix: usize) usize { 217 | const token = self.tokens.items[token_ix]; 218 | return switch (token) { 219 | .l_paren, .l_brace, .l_bracket => 4, 220 | else => 0, 221 | }; 222 | } 223 | }; 224 | -------------------------------------------------------------------------------- /lib/focus/language/generic.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const style = focus.style; 6 | const language = focus.language; 7 | 8 | pub const State = struct { 9 | allocator: u.Allocator, 10 | comment_string: []const u8, 11 | tokens: []const Token, 12 | token_ranges: []const [2]usize, 13 | paren_levels: []const usize, 14 | paren_parents: []const ?usize, 15 | paren_matches: []const ?usize, 16 | mode: enum { 17 | Normal, 18 | Parens, 19 | }, 20 | 21 | pub fn init(allocator: u.Allocator, comment_string: []const u8, source: []const u8) State { 22 | var tokens = u.ArrayList(Token).init(allocator); 23 | var token_ranges = u.ArrayList([2]usize).init(allocator); 24 | var tokenizer = Tokenizer.init(comment_string, source); 25 | while (true) { 26 | const start = tokenizer.pos; 27 | const token = tokenizer.next(); 28 | const end = tokenizer.pos; 29 | if (token == .eof) break; 30 | tokens.append(token) catch u.oom(); 31 | token_ranges.append(.{ start, end }) catch u.oom(); 32 | } 33 | 34 | const paren_levels = allocator.alloc(usize, tokens.items.len) catch u.oom(); 35 | @memset(paren_levels, 0); 36 | const paren_parents = allocator.alloc(?usize, tokens.items.len) catch u.oom(); 37 | @memset(paren_parents, null); 38 | const paren_matches = allocator.alloc(?usize, tokens.items.len) catch u.oom(); 39 | @memset(paren_matches, null); 40 | var paren_match_stack = u.ArrayList(usize).init(allocator); 41 | for (tokens.items, 0..) |token, ix| { 42 | switch (token) { 43 | .close_paren, .close_bracket, .close_brace => { 44 | if (paren_match_stack.pop()) |matching_ix| { 45 | paren_matches[ix] = matching_ix; 46 | paren_matches[matching_ix] = ix; 47 | } 48 | }, 49 | else => {}, 50 | } 51 | if (paren_match_stack.items.len > 0) 52 | paren_parents[ix] = paren_match_stack.items[paren_match_stack.items.len - 1]; 53 | paren_levels[ix] = paren_match_stack.items.len; 54 | switch (token) { 55 | .open_paren, .open_bracket, .open_brace => { 56 | paren_match_stack.append(ix) catch u.oom(); 57 | }, 58 | else => {}, 59 | } 60 | } 61 | 62 | return .{ 63 | .allocator = allocator, 64 | .comment_string = comment_string, 65 | .tokens = tokens.toOwnedSlice() catch u.oom(), 66 | .token_ranges = token_ranges.toOwnedSlice() catch u.oom(), 67 | .paren_levels = paren_levels, 68 | .paren_parents = paren_parents, 69 | .paren_matches = paren_matches, 70 | .mode = .Normal, 71 | }; 72 | } 73 | 74 | pub fn deinit(self: *State) void { 75 | self.allocator.free(self.paren_matches); 76 | self.allocator.free(self.paren_parents); 77 | self.allocator.free(self.paren_levels); 78 | self.allocator.free(self.token_ranges); 79 | self.allocator.free(self.tokens); 80 | self.* = undefined; 81 | } 82 | 83 | pub fn updateBeforeChange(self: *State, source: []const u8, delete_range: [2]usize) void { 84 | _ = self; 85 | _ = source; 86 | _ = delete_range; 87 | } 88 | 89 | pub fn updateAfterChange(self: *State, source: []const u8, insert_range: [2]usize) void { 90 | _ = insert_range; 91 | const allocator = self.allocator; 92 | const comment_string = self.comment_string; 93 | const mode = self.mode; 94 | self.deinit(); 95 | self.* = State.init(allocator, comment_string, source); 96 | self.mode = mode; 97 | } 98 | 99 | pub fn toggleMode(self: *State) void { 100 | self.mode = switch (self.mode) { 101 | .Normal => .Parens, 102 | .Parens => .Normal, 103 | }; 104 | } 105 | 106 | pub fn highlight(self: State, source: []const u8, range: [2]usize, colors: []u.Color) void { 107 | @memset(colors, style.comment_color); 108 | for (self.token_ranges, 0..) |token_range, i| { 109 | const source_start = token_range[0]; 110 | const source_end = token_range[1]; 111 | if (source_end < range[0] or source_start > range[1]) continue; 112 | const colors_start = if (source_start > range[0]) source_start - range[0] else 0; 113 | const colors_end = if (source_end > range[1]) range[1] - range[0] else source_end - range[0]; 114 | const token = self.tokens[i]; 115 | const color = switch (token) { 116 | .identifier => if (self.mode == .Parens) style.comment_color else style.identColor(source[source_start..source_end]), 117 | .comment, .whitespace => if (self.mode == .Parens) style.comment_color else style.comment_color, 118 | .open_paren, .open_bracket, .open_brace, .close_paren, .close_bracket, .close_brace => color: { 119 | var is_good_match = false; 120 | if (self.paren_matches[i]) |matching_ix| { 121 | const matching_token = self.tokens[matching_ix]; 122 | is_good_match = switch (token) { 123 | .open_paren => matching_token == .close_paren, 124 | .open_bracket => matching_token == .close_bracket, 125 | .open_brace => matching_token == .close_brace, 126 | .close_paren => matching_token == .open_paren, 127 | .close_bracket => matching_token == .open_bracket, 128 | .close_brace => matching_token == .open_brace, 129 | else => unreachable, 130 | }; 131 | } 132 | break :color if (is_good_match) 133 | if (self.mode == .Parens) 134 | style.parenColor(self.paren_levels[i]) 135 | else 136 | style.comment_color 137 | else 138 | style.emphasisRed; 139 | }, 140 | else => if (self.mode == .Parens) style.comment_color else style.keyword_color, 141 | }; 142 | @memset(colors[colors_start..colors_end], color); 143 | } 144 | } 145 | 146 | pub fn getAddedIndent(self: State, token_ix: usize) usize { 147 | const token = self.tokens[token_ix]; 148 | return switch (token) { 149 | .open_paren, .open_brace, .open_bracket => 4, 150 | else => 0, 151 | }; 152 | } 153 | }; 154 | 155 | pub const Token = enum { 156 | identifier, 157 | number, 158 | string, 159 | comment, 160 | whitespace, 161 | open_paren, 162 | close_paren, 163 | open_brace, 164 | close_brace, 165 | open_bracket, 166 | close_bracket, 167 | unknown, 168 | eof, 169 | }; 170 | 171 | const TokenizerState = enum { 172 | start, 173 | identifier, 174 | number, 175 | string, 176 | string_escape, 177 | comment, 178 | whitespace, 179 | }; 180 | 181 | pub const Tokenizer = struct { 182 | comment_string: []const u8, 183 | source: []const u8, 184 | pos: usize, 185 | 186 | pub fn init(comment_string: []const u8, source: []const u8) Tokenizer { 187 | return .{ 188 | .comment_string = comment_string, 189 | .source = source, 190 | .pos = 0, 191 | }; 192 | } 193 | 194 | pub fn next(self: *Tokenizer) Token { 195 | var state = TokenizerState.start; 196 | var quote_char: ?u8 = null; 197 | const source_len = self.source.len; 198 | while (true) { 199 | if (state == .start and 200 | self.pos < source_len and 201 | source_len - self.pos > self.comment_string.len and 202 | u.deepEqual(self.source[self.pos .. self.pos + self.comment_string.len], self.comment_string)) 203 | { 204 | state = .comment; 205 | self.pos += self.comment_string.len; 206 | continue; 207 | } 208 | 209 | const char = if (self.pos < source_len) self.source[self.pos] else 0; 210 | self.pos += 1; 211 | switch (state) { 212 | .start => switch (char) { 213 | 0 => { 214 | self.pos -= 1; 215 | return .eof; 216 | }, 217 | 'a'...'z', 'A'...'Z' => state = .identifier, 218 | '-', '0'...'9' => state = .number, 219 | '"', '\'' => { 220 | quote_char = char; 221 | state = .string; 222 | }, 223 | ' ', '\r', '\n', '\t' => state = .whitespace, 224 | '(' => return .open_paren, 225 | ')' => return .close_paren, 226 | '{' => return .open_brace, 227 | '}' => return .close_brace, 228 | '[' => return .open_bracket, 229 | ']' => return .close_bracket, 230 | else => return .unknown, 231 | }, 232 | .identifier => switch (char) { 233 | 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => {}, 234 | else => { 235 | self.pos -= 1; 236 | return .identifier; 237 | }, 238 | }, 239 | .number => switch (char) { 240 | '0'...'9', 'a'...'z', 'A'...'Z', '.', '-' => state = .number, 241 | else => { 242 | self.pos -= 1; 243 | return .number; 244 | }, 245 | }, 246 | .string => switch (char) { 247 | 0 => { 248 | self.pos -= 1; 249 | return .unknown; 250 | }, 251 | '\\' => state = .string_escape, 252 | else => { 253 | if (char == quote_char) return .string; 254 | }, 255 | }, 256 | .string_escape => switch (char) { 257 | 0 => { 258 | self.pos -= 1; 259 | return .unknown; 260 | }, 261 | else => state = .string, 262 | }, 263 | .comment => switch (char) { 264 | 0, '\n' => { 265 | self.pos -= 1; 266 | return .comment; 267 | }, 268 | else => {}, 269 | }, 270 | .whitespace => switch (char) { 271 | ' ', '\r', '\t' => {}, 272 | else => { 273 | self.pos -= 1; 274 | return .whitespace; 275 | }, 276 | }, 277 | } 278 | } 279 | } 280 | }; 281 | -------------------------------------------------------------------------------- /lib/focus/maker.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const App = focus.App; 6 | const Buffer = focus.Buffer; 7 | const Editor = focus.Editor; 8 | const Window = focus.Window; 9 | const style = focus.style; 10 | const SingleLineEditor = focus.SingleLineEditor; 11 | const Selector = focus.Selector; 12 | const ErrorLister = focus.ErrorLister; 13 | const ChildProcess = focus.ChildProcess; 14 | const mach_compat = focus.mach_compat; 15 | 16 | pub const Maker = struct { 17 | app: *App, 18 | input: SingleLineEditor, 19 | selector: Selector, 20 | result_editor: *Editor, 21 | history_string: []const u8, 22 | history: []const []const u8, 23 | state: union(enum) { 24 | ChoosingDir, 25 | ChoosingCommand: struct { 26 | dirname: []const u8, 27 | }, 28 | Running: struct { 29 | dirname: []const u8, 30 | command: []const u8, 31 | child_process: ChildProcess, 32 | error_locations: u.ArrayList(ErrorLister.ErrorLocation), 33 | 34 | const Self = @This(); 35 | fn clearErrorLocations(self: *Self, allocator: u.Allocator) void { 36 | for (self.error_locations.items) |error_location| 37 | error_location.deinit(allocator); 38 | self.error_locations.shrinkRetainingCapacity(0); 39 | } 40 | fn deinit(self: *Self, allocator: u.Allocator) void { 41 | for (self.error_locations.items) |error_location| 42 | error_location.deinit(allocator); 43 | self.error_locations.deinit(); 44 | self.child_process.deinit(); 45 | allocator.free(self.command); 46 | allocator.free(self.dirname); 47 | } 48 | }, 49 | }, 50 | 51 | pub fn init(app: *App) *Maker { 52 | const empty_buffer = Buffer.initEmpty(app, .{ 53 | .enable_completions = false, 54 | .enable_undo = false, 55 | }); 56 | const result_editor = Editor.init(app, empty_buffer, .{ 57 | .show_status_bar = false, 58 | .show_completer = false, 59 | }); 60 | const input = SingleLineEditor.init(app, focus.config.home_path); 61 | const selector = Selector.init(app); 62 | 63 | const result = std.process.Child.run(.{ 64 | .allocator = app.frame_allocator, 65 | .argv = &[_][]const u8{ "fish", "--command", "history" }, 66 | .cwd = focus.config.home_path, 67 | .max_output_bytes = 128 * 1024 * 1024, 68 | }) catch |err| u.panic("{} while calling fish history", .{err}); 69 | u.assert(result.term == .Exited and result.term.Exited == 0); 70 | const history_string = app.dupe(result.stdout); 71 | var history = u.ArrayList([]const u8).init(app.allocator); 72 | var lines = std.mem.splitScalar(u8, history_string, '\n'); 73 | while (lines.next()) |line| { 74 | history.append(app.dupe(line)) catch u.oom(); 75 | } 76 | 77 | const self = app.allocator.create(Maker) catch u.oom(); 78 | self.* = Maker{ 79 | .app = app, 80 | .input = input, 81 | .selector = selector, 82 | .result_editor = result_editor, 83 | .history_string = history_string, 84 | .history = history.toOwnedSlice() catch u.oom(), 85 | .state = .ChoosingDir, 86 | }; 87 | return self; 88 | } 89 | 90 | pub fn deinit(self: *Maker) void { 91 | switch (self.state) { 92 | .ChoosingDir => return, 93 | .ChoosingCommand => |choosing_command| self.app.allocator.free(choosing_command.dirname), 94 | .Running => |*running| running.deinit(self.app.allocator), 95 | } 96 | self.app.allocator.free(self.history); 97 | self.app.allocator.free(self.history_string); 98 | self.selector.deinit(); 99 | self.input.deinit(); 100 | const buffer = self.result_editor.buffer; 101 | self.result_editor.deinit(); 102 | buffer.deinit(); 103 | self.app.allocator.destroy(self); 104 | } 105 | 106 | pub fn frame(self: *Maker, window: *Window, rect: u.Rect, events: []const mach_compat.Event) void { 107 | switch (self.state) { 108 | .ChoosingDir => { 109 | const layout = window.layoutSearcher(rect); 110 | 111 | // run input frame 112 | const input_changed = self.input.frame(window, layout.input, events); 113 | if (input_changed == .Changed) self.selector.selected = 0; 114 | 115 | // get and filter completions 116 | const results_or_err = u.fuzzy_search_paths(self.app.frame_allocator, self.input.getText()); 117 | const results = results_or_err catch &[_][]const u8{}; 118 | 119 | // run selector frame 120 | var action: Selector.Action = .None; 121 | if (results_or_err) |_| { 122 | self.selector.setItems(results); 123 | action = self.selector.frame(window, layout.selector, events); 124 | } else |results_err| { 125 | const error_text = u.format(self.app.frame_allocator, "Error opening directory: {}", .{results_err}); 126 | window.queueText(layout.selector, style.emphasisRed, error_text); 127 | } 128 | 129 | const path = self.input.getText(); 130 | const dirname = if (path.len > 0 and std.fs.path.isSep(path[path.len - 1])) 131 | path[0 .. path.len - 1] 132 | else 133 | std.fs.path.dirname(path) orelse ""; 134 | 135 | // maybe enter dir 136 | if (action == .SelectOne) { 137 | self.input.buffer.replace(std.fs.path.join(self.app.frame_allocator, &[_][]const u8{ dirname, results[self.selector.selected] }) catch u.oom()); 138 | const cursor = self.input.editor.getMainCursor(); 139 | self.input.editor.goRealLineEnd(cursor); 140 | } 141 | // maybe choose current dir 142 | if (action == .SelectRaw) { 143 | const chosen_dirname: []const u8 = self.input.getText(); 144 | if (chosen_dirname.len > 0 and std.fs.path.isSep(chosen_dirname[chosen_dirname.len - 1])) { 145 | self.input.buffer.replace(""); 146 | self.state = .{ .ChoosingCommand = .{ 147 | .dirname = self.app.dupe(chosen_dirname), 148 | } }; 149 | } 150 | } 151 | }, 152 | .ChoosingCommand => |choosing_command| { 153 | const layout = window.layoutSearcher(rect); 154 | 155 | // run input frame 156 | const input_changed = self.input.frame(window, layout.input, events); 157 | if (input_changed == .Changed) self.selector.selected = 0; 158 | 159 | // filter lines 160 | const filtered_history = u.fuzzy_search(self.app.frame_allocator, self.history, self.input.getText()); 161 | 162 | // run selector frame 163 | var action: Selector.Action = .None; 164 | self.selector.setItems(filtered_history); 165 | action = self.selector.frame(window, layout.selector, events); 166 | 167 | // maybe run command 168 | var command_o: ?[]const u8 = null; 169 | if (action == .SelectOne) { 170 | command_o = self.app.dupe(filtered_history[self.selector.selected]); 171 | } 172 | if (action == .SelectRaw) { 173 | command_o = self.app.dupe(self.input.getText()); 174 | } 175 | 176 | if (command_o) |command| { 177 | // add command to history 178 | const history_path = std.fmt.allocPrint( 179 | self.app.frame_allocator, 180 | "{s}/.local/share/fish/fish_history", 181 | .{focus.config.home_path}, 182 | ) catch u.oom(); 183 | const history_file = std.fs.cwd().openFile(history_path, .{ .mode = .write_only }) catch |err| 184 | u.panic("Failed to open fish history: {}", .{err}); 185 | history_file.seekFromEnd(0) catch |err| 186 | u.panic("Failed to seek to end of fish history: {}", .{err}); 187 | std.fmt.format(history_file.writer(), "\n- cmd: {s}\n when: {}", .{ command, std.time.timestamp() }) catch |err| 188 | u.panic("Failed to write to fish history: {}", .{err}); 189 | history_file.close(); 190 | 191 | // start running 192 | self.state = .{ .Running = .{ 193 | .dirname = choosing_command.dirname, 194 | .command = command, 195 | .child_process = ChildProcess.init( 196 | self.app.allocator, 197 | choosing_command.dirname, 198 | &.{ "fish", "--command", command }, 199 | ), 200 | .error_locations = u.ArrayList(ErrorLister.ErrorLocation).init(self.app.allocator), 201 | } }; 202 | } 203 | }, 204 | .Running => |*running| { 205 | const new_text = running.child_process.read(self.app.frame_allocator); 206 | if (new_text.len > 0) { 207 | self.result_editor.buffer.insert(self.result_editor.buffer.getBufferEnd(), new_text); 208 | // parse results 209 | running.clearErrorLocations(self.app.allocator); 210 | const error_locations = &running.error_locations; 211 | const text = self.result_editor.buffer.bytes.items; 212 | var i: usize = 0; 213 | next_match: while (i < text.len) { 214 | // (whitespace|":")!+ ":" 215 | const path_start = i; 216 | while (i < text.len) { 217 | switch (text[i]) { 218 | ' ', '\t', '\r', '\n', ':' => { 219 | break; 220 | }, 221 | else => { 222 | i += 1; 223 | }, 224 | } 225 | } 226 | const path_end = i; 227 | if (path_start == path_end) { 228 | i += 1; 229 | continue :next_match; 230 | } 231 | 232 | if (i < text.len and text[i] == ':') 233 | i += 1 234 | else 235 | continue :next_match; 236 | 237 | // digit+ : 238 | const line_start = i; 239 | while (i < text.len) { 240 | switch (text[i]) { 241 | '0'...'9' => { 242 | i += 1; 243 | }, 244 | else => { 245 | break; 246 | }, 247 | } 248 | } 249 | const line_end = i; 250 | if (line_start == line_end) { 251 | continue :next_match; 252 | } 253 | 254 | // (":" digit+)? 255 | var col_start: ?usize = null; 256 | var col_end: ?usize = null; 257 | if (i < text.len and text[i] == ':') { 258 | i += 1; 259 | col_start = i; 260 | while (i < text.len) { 261 | switch (text[i]) { 262 | '0'...'9' => { 263 | i += 1; 264 | }, 265 | else => { 266 | break; 267 | }, 268 | } 269 | } 270 | col_end = i; 271 | if (col_start.? == col_end.?) { 272 | continue :next_match; 273 | } 274 | } 275 | 276 | const path = text[path_start..path_end]; 277 | const full_path = std.fs.path.resolve(self.app.allocator, &.{ 278 | running.dirname, 279 | path, 280 | }) catch continue :next_match; 281 | 282 | const line = std.fmt.parseInt( 283 | usize, 284 | text[line_start..line_end], 285 | 10, 286 | ) catch continue :next_match; 287 | 288 | const col = if (col_start == null) 289 | 1 290 | else 291 | std.fmt.parseInt( 292 | usize, 293 | text[col_start.?..col_end.?], 294 | 10, 295 | ) catch continue :next_match; 296 | 297 | error_locations.append(.{ 298 | .report_buffer = self.result_editor.buffer, 299 | .report_location = .{ path_start, i }, 300 | .path = full_path, 301 | .line = line, 302 | .col = col, 303 | }) catch u.oom(); 304 | } 305 | } 306 | 307 | // show results in editor 308 | self.result_editor.frame(window, rect, events); 309 | }, 310 | } 311 | } 312 | 313 | pub fn handleAfterSave(self: *Maker) void { 314 | switch (self.state) { 315 | .ChoosingDir, .ChoosingCommand => return, 316 | .Running => |*running| { 317 | self.result_editor.buffer.replace(""); 318 | running.clearErrorLocations(self.app.allocator); 319 | running.child_process.deinit(); 320 | running.child_process = ChildProcess.init( 321 | self.app.allocator, 322 | running.dirname, 323 | &.{ "fish", "--command", running.command }, 324 | ); 325 | }, 326 | } 327 | } 328 | }; 329 | -------------------------------------------------------------------------------- /lib/focus/language.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const style = focus.style; 6 | 7 | pub const zig = @import("./language/zig.zig"); 8 | pub const clojure = @import("./language/clojure.zig"); 9 | pub const deno = @import("./language/deno.zig"); 10 | pub const go = @import("./language/go.zig"); 11 | pub const rust = @import("./language/rust.zig"); 12 | pub const generic = @import("./language/generic.zig"); 13 | 14 | pub const Language = union(enum) { 15 | Zig: zig.State, 16 | Clojure: clojure.State, 17 | Java: generic.State, 18 | Shell: generic.State, 19 | Julia: generic.State, 20 | Javascript: deno.State, 21 | Typescript: deno.State, 22 | Nix: generic.State, 23 | C: generic.State, 24 | Go: go.State, 25 | Zest: generic.State, 26 | Rust: rust.State, 27 | Unknown, 28 | 29 | pub const Squiggly = struct { 30 | color: u.Color, 31 | range: [2]usize, 32 | }; 33 | 34 | pub fn init(allocator: u.Allocator, filename: []const u8, source: []const u8) Language { 35 | return if (std.mem.endsWith(u8, filename, ".zig")) 36 | .{ .Zig = zig.State.init(allocator, source) } 37 | else if (std.mem.endsWith(u8, filename, ".clj") or 38 | std.mem.endsWith(u8, filename, ".cljs") or 39 | std.mem.endsWith(u8, filename, ".cljc") or 40 | std.mem.endsWith(u8, filename, ".edn")) 41 | .{ .Clojure = clojure.State.init(allocator, source) } 42 | else if (std.mem.endsWith(u8, filename, ".java")) 43 | .{ .Java = generic.State.init(allocator, "//", source) } 44 | else if (std.mem.endsWith(u8, filename, ".sh")) 45 | .{ .Shell = generic.State.init(allocator, "#", source) } 46 | else if (std.mem.endsWith(u8, filename, ".jl")) 47 | .{ .Julia = generic.State.init(allocator, "#", source) } 48 | else if (std.mem.endsWith(u8, filename, ".js")) 49 | .{ .Javascript = deno.State.init(allocator, source) } 50 | else if (std.mem.endsWith(u8, filename, ".ts")) 51 | .{ .Typescript = deno.State.init(allocator, source) } 52 | else if (std.mem.endsWith(u8, filename, ".nix")) 53 | .{ .Nix = generic.State.init(allocator, "#", source) } 54 | else if (std.mem.endsWith(u8, filename, ".c") or std.mem.endsWith(u8, filename, ".h")) 55 | .{ .C = generic.State.init(allocator, "//", source) } 56 | else if (std.mem.endsWith(u8, filename, ".go") or (std.mem.endsWith(u8, filename, ".go.tmpl"))) 57 | .{ .Go = go.State.init(allocator, source) } 58 | else if (std.mem.endsWith(u8, filename, ".zest")) 59 | .{ .Zest = generic.State.init(allocator, "//", source) } 60 | else if (std.mem.endsWith(u8, filename, ".rs")) 61 | .{ .Rust = rust.State.init(allocator, source) } 62 | else 63 | .Unknown; 64 | } 65 | 66 | pub fn deinit(self: *Language) void { 67 | switch (self.*) { 68 | .Zig => |*state| state.deinit(), 69 | .Clojure => |*state| state.deinit(), 70 | else => {}, 71 | } 72 | } 73 | 74 | pub fn updateBeforeChange(self: *Language, source: []const u8, delete_range: [2]usize) void { 75 | switch (self.*) { 76 | .Unknown => {}, 77 | inline else => |*state| state.updateBeforeChange(source, delete_range), 78 | } 79 | } 80 | 81 | pub fn updateAfterChange(self: *Language, source: []const u8, insert_range: [2]usize) void { 82 | switch (self.*) { 83 | .Unknown => {}, 84 | inline else => |*state| state.updateAfterChange(source, insert_range), 85 | } 86 | } 87 | 88 | pub fn toggleMode(self: *Language) void { 89 | switch (self.*) { 90 | .Unknown => {}, 91 | inline else => |*state| state.toggleMode(), 92 | } 93 | } 94 | 95 | pub fn getCommentString(self: Language) ?[]const u8 { 96 | return switch (self) { 97 | .Zig => "//", 98 | .Clojure => ";", 99 | .Javascript, .Typescript, .Go, .Rust => "//", 100 | .Java, .Shell, .Julia, .Nix, .C, .Zest => |*state| state.comment_string, 101 | .Unknown => null, 102 | }; 103 | } 104 | 105 | pub fn highlight(self: Language, allocator: u.Allocator, source: []const u8, range: [2]usize) []const u.Color { 106 | const colors = allocator.alloc(u.Color, range[1] - range[0]) catch u.oom(); 107 | @memset(colors, style.text_color); 108 | switch (self) { 109 | .Unknown => {}, 110 | inline else => |state| state.highlight(source, range, colors), 111 | } 112 | return colors; 113 | } 114 | 115 | pub fn getTokenRanges(self: Language) []const [2]usize { 116 | return switch (self) { 117 | inline .Javascript, .Typescript, .Go, .Rust => |state| state.generic.token_ranges, 118 | .Zig => |state| state.token_ranges.items, 119 | .Unknown => &[0][2]usize{}, 120 | inline else => |state| state.token_ranges, 121 | }; 122 | } 123 | 124 | pub fn getParenLevels(self: Language) []const usize { 125 | return switch (self) { 126 | inline .Javascript, .Typescript, .Go, .Rust => |state| state.generic.paren_levels, 127 | .Zig => |state| state.paren_levels.items, 128 | .Unknown => &[0]?usize{}, 129 | inline else => |state| state.paren_levels, 130 | }; 131 | } 132 | 133 | pub fn getParenParents(self: Language) []const ?usize { 134 | return switch (self) { 135 | inline .Javascript, .Typescript, .Go, .Rust => |state| state.generic.paren_parents, 136 | .Zig => |state| state.paren_parents.items, 137 | .Unknown => &[0]?usize{}, 138 | inline else => |state| state.paren_parents, 139 | }; 140 | } 141 | 142 | pub fn getParenMatches(self: Language) []const ?usize { 143 | return switch (self) { 144 | inline .Javascript, .Typescript, .Go, .Rust => |state| state.generic.paren_matches, 145 | .Zig => |state| state.paren_matches.items, 146 | .Unknown => &[0]?usize{}, 147 | inline else => |state| state.paren_matches, 148 | }; 149 | } 150 | 151 | pub fn getTokenIxBefore(self: Language, pos: usize) ?usize { 152 | const token_ranges = self.getTokenRanges(); 153 | var return_ix: ?usize = null; 154 | for (token_ranges, 0..) |token_range, ix| { 155 | if (token_range[0] >= pos) break; 156 | return_ix = ix; 157 | } 158 | return return_ix; 159 | } 160 | 161 | pub fn getTokenIxAfter(self: Language, pos: usize) ?usize { 162 | const token_ranges = self.getTokenRanges(); 163 | for (token_ranges, 0..) |token_range, ix| { 164 | if (token_range[0] >= pos) 165 | return ix; 166 | } 167 | return null; 168 | } 169 | 170 | pub fn getIdentifierRangeAt(self: Language, pos: usize) ?[2]usize { 171 | const token_ix = self.getTokenIxBefore(pos) orelse self.getTokenIxAfter(pos) orelse return null; 172 | switch (self) { 173 | .Zig => |state| return if (state.tokens.items[token_ix] == .identifier) (state.token_ranges.items[token_ix]) else null, 174 | .Zest => |state| return if (state.tokens[token_ix] == .identifier) (state.token_ranges[token_ix]) else null, 175 | inline .Go, .Rust => |state| return if (state.generic.tokens[token_ix] == .identifier) (state.generic.token_ranges[token_ix]) else null, 176 | else => return null, 177 | } 178 | } 179 | 180 | pub fn format(self: Language, frame_allocator: u.Allocator, source: []const u8) ?[]const u8 { 181 | return switch (self) { 182 | inline .Zig, .Javascript, .Typescript, .Go, .Rust => |state| state.format(frame_allocator, source), 183 | .Unknown => null, 184 | inline else => stripTrailingWhitespace(frame_allocator, source), 185 | }; 186 | } 187 | 188 | pub fn stripTrailingWhitespace(frame_allocator: u.Allocator, source: []const u8) []const u8 { 189 | var new_source = u.ArrayList(u8).init(frame_allocator); 190 | 191 | var line_start: usize = 0; 192 | var line_end: usize = 0; 193 | var i: usize = 0; 194 | while (true) { 195 | if (i >= source.len) { 196 | new_source.appendSlice(source[line_start..line_end]) catch u.oom(); 197 | break; 198 | } else if (source[i] == '\n') { 199 | new_source.appendSlice(source[line_start..line_end]) catch u.oom(); 200 | new_source.append('\n') catch u.oom(); 201 | i += 1; 202 | line_start = i; 203 | line_end = i; 204 | } else { 205 | if (source[i] != ' ') 206 | line_end = i + 1; 207 | i += 1; 208 | } 209 | } 210 | return new_source.toOwnedSlice() catch u.oom(); 211 | } 212 | 213 | pub fn matchParen(self: Language, pos: usize) ?usize { 214 | if (self.getTokenIxBefore(pos)) |token_ix| { 215 | const token_range = self.getTokenRanges()[token_ix]; 216 | if (token_range[1] >= pos) 217 | if (self.getParenMatches()[token_ix]) |matching_ix| 218 | return self.getTokenRanges()[matching_ix][1]; 219 | } 220 | return null; 221 | } 222 | 223 | pub fn getAddedIndent(self: Language, token_ix: usize) usize { 224 | return switch (self) { 225 | .Unknown => 0, 226 | inline else => |state| state.getAddedIndent(token_ix), 227 | }; 228 | } 229 | 230 | pub fn getIdealIndent(self: Language, source: []const u8, line_start_pos: usize, line_end_pos: usize) usize { 231 | const anchor: struct { 232 | ix: usize, 233 | added_indent: usize, 234 | } = anchor: { 235 | if (self.getTokenIxAfter(line_start_pos)) |after_ix| 236 | if (self.getParenMatches()[after_ix]) |matching_ix| 237 | if (matching_ix < after_ix) 238 | if (self.getTokenRanges()[after_ix][0] < line_end_pos) 239 | // line starts with closing paren 240 | // align with the opening paren 241 | break :anchor .{ .ix = matching_ix, .added_indent = 0 }; 242 | 243 | if (self.getTokenIxBefore(line_start_pos)) |before_ix| { 244 | const added_indent = self.getAddedIndent(before_ix); 245 | if (added_indent != 0) 246 | // prev line ends with opening paren 247 | // indent from the opening paren 248 | break :anchor .{ .ix = before_ix, .added_indent = added_indent }; 249 | 250 | if (self.getParenParents()[before_ix]) |parent_ix| 251 | // prev line is in some paren pair 252 | // indent with the opening paren of that pair 253 | break :anchor .{ .ix = parent_ix, .added_indent = self.getAddedIndent(parent_ix) }; 254 | } 255 | 256 | // nothing to align with 257 | // give up 258 | return 0; 259 | }; 260 | 261 | const anchor_range = self.getTokenRanges()[anchor.ix]; 262 | const anchor_line_start = if (std.mem.lastIndexOfScalar(u8, source[0..anchor_range[0]], '\n')) |n| 263 | n + 1 264 | else 265 | 0; 266 | var anchor_text_start = anchor_line_start; 267 | // TODO handle alternative whitespace 268 | while (anchor_text_start < anchor_range[0] and source[anchor_text_start] == ' ') : (anchor_text_start += 1) {} 269 | const anchor_indent = anchor_text_start - anchor_line_start; 270 | const anchor_pos = anchor_range[1] - 1 - anchor_line_start; 271 | 272 | return switch (self) { 273 | .Clojure => anchor_pos + anchor.added_indent, 274 | else => anchor_indent + anchor.added_indent, 275 | }; 276 | } 277 | 278 | pub fn getSquigglies(self: Language) []const Language.Squiggly { 279 | switch (self) { 280 | .Zig => |state| return state.squigglies.items, 281 | else => return &[0]Language.Squiggly{}, 282 | } 283 | } 284 | 285 | pub fn afterLoad(self: Language, frame_allocator: u.Allocator, source: []const u8) []const u8 { 286 | switch (self) { 287 | .Go => |state| return state.afterLoad(frame_allocator, source), 288 | else => return source, 289 | } 290 | } 291 | 292 | pub fn beforeSave(self: Language, frame_allocator: u.Allocator, source: []const u8) []const u8 { 293 | return switch (self) { 294 | .Go => |state| return state.beforeSave(frame_allocator, source), 295 | else => source, 296 | }; 297 | } 298 | 299 | pub fn writeDefinitionSearchString(self: Language, writer: anytype, identifier: []const u8) !void { 300 | switch (self) { 301 | .Zig => try std.fmt.format( 302 | writer, 303 | \\fn {s}\(|const {s} =|var {s} =| {s}: 304 | , 305 | .{ identifier, identifier, identifier, identifier }, 306 | ), 307 | .Go => try std.fmt.format( 308 | writer, 309 | \\func.* {s}\(|type {s} |\t{s}\s*= 310 | , 311 | .{ identifier, identifier, identifier }, 312 | ), 313 | else => {}, 314 | } 315 | } 316 | 317 | pub fn shouldDoubleNewline(self: Language, pos: usize) bool { 318 | if (self == .Unknown or self == .Clojure) 319 | return false; 320 | 321 | if (self.getTokenIxAfter(pos)) |after_ix| 322 | if (self.getParenMatches()[after_ix]) |before_ix| 323 | if (before_ix < after_ix) 324 | if (self.getTokenRanges()[after_ix][0] == pos) 325 | // the next character is a closing paren 326 | return true; 327 | 328 | return false; 329 | } 330 | 331 | const TransformCharInput = struct { 332 | insert: []const u8, 333 | move: isize, 334 | }; 335 | 336 | pub fn transformCharInput(self: Language, source: []const u8, pos: usize, char_input: []const u8) TransformCharInput { 337 | if (self == .Unknown) 338 | return .{ .insert = char_input, .move = 0 }; 339 | 340 | if (std.mem.eql(u8, char_input, "(")) 341 | return .{ .insert = "()", .move = -1 }; 342 | 343 | if (std.mem.eql(u8, char_input, "[")) 344 | return .{ .insert = "[]", .move = -1 }; 345 | 346 | if (std.mem.eql(u8, char_input, "{")) 347 | return .{ .insert = "{}", .move = -1 }; 348 | 349 | if (std.mem.eql(u8, char_input, ")")) 350 | if (pos < source.len and source[pos] == ')') 351 | return .{ .insert = "", .move = 1 }; 352 | 353 | if (std.mem.eql(u8, char_input, "]")) 354 | if (pos < source.len and source[pos] == ']') 355 | return .{ .insert = "", .move = 1 }; 356 | 357 | if (std.mem.eql(u8, char_input, "}")) 358 | if (pos < source.len and source[pos] == '}') 359 | return .{ .insert = "", .move = 1 }; 360 | 361 | if (std.mem.eql(u8, char_input, "\"")) 362 | return if (pos > 0 and source[pos - 1] == '\\') 363 | .{ .insert = "\"", .move = 0 } 364 | else if (pos < source.len and source[pos] == '"') 365 | .{ .insert = "", .move = 1 } 366 | else 367 | .{ .insert = "\"\"", .move = -1 }; 368 | 369 | if (std.mem.eql(u8, char_input, "'") and self != .Rust) 370 | return if (pos > 0 and source[pos - 1] == '\\') 371 | .{ .insert = "'", .move = 0 } 372 | else if (pos < source.len and source[pos] == '\'') 373 | .{ .insert = "", .move = 1 } 374 | else 375 | .{ .insert = "''", .move = -1 }; 376 | 377 | if (self == .Zig or self == .Rust) { 378 | if (std.mem.eql(u8, char_input, "|")) 379 | return if (pos < source.len and source[pos] == '|') 380 | .{ .insert = "", .move = 1 } 381 | else 382 | .{ .insert = "||", .move = -1 }; 383 | } 384 | 385 | return .{ .insert = char_input, .move = 0 }; 386 | } 387 | 388 | pub fn shouldDeleteMatchingParen(self: Language, source: []const u8, pos: usize) bool { 389 | if (self == .Unknown) 390 | return false; 391 | 392 | if (pos == 0 or pos >= source.len) 393 | return false; 394 | 395 | return switch (source[pos - 1]) { 396 | '(' => source[pos] == ')', 397 | '[' => source[pos] == ']', 398 | '{' => source[pos] == '}', 399 | '"' => source[pos] == '"', 400 | '\'' => source[pos] == '\'', 401 | else => false, 402 | }; 403 | } 404 | }; 405 | -------------------------------------------------------------------------------- /lib/focus.zig: -------------------------------------------------------------------------------- 1 | pub const util = @import("./focus/util.zig"); 2 | pub const config = @import("focus_config"); 3 | pub const Atlas = @import("./focus/atlas.zig").Atlas; 4 | pub const Buffer = @import("./focus/buffer.zig").Buffer; 5 | pub const LineWrappedBuffer = @import("./focus/line_wrapped_buffer.zig").LineWrappedBuffer; 6 | pub const Editor = @import("./focus/editor.zig").Editor; 7 | pub const SingleLineEditor = @import("./focus/single_line_editor.zig").SingleLineEditor; 8 | pub const Selector = @import("./focus/selector.zig").Selector; 9 | pub const FileOpener = @import("./focus/file_opener.zig").FileOpener; 10 | pub const ProjectFileOpener = @import("./focus/project_file_opener.zig").ProjectFileOpener; 11 | pub const BufferOpener = @import("./focus/buffer_opener.zig").BufferOpener; 12 | pub const BufferSearcher = @import("./focus/buffer_searcher.zig").BufferSearcher; 13 | pub const ProjectSearcher = @import("./focus/project_searcher.zig").ProjectSearcher; 14 | pub const Launcher = @import("./focus/launcher.zig").Launcher; 15 | pub const Maker = @import("./focus/maker.zig").Maker; 16 | pub const ErrorLister = @import("./focus/error_lister.zig").ErrorLister; 17 | pub const Window = @import("./focus/window.zig").Window; 18 | pub const Language = @import("./focus/language.zig").Language; 19 | pub const ChildProcess = @import("./focus/child_process.zig").ChildProcess; 20 | pub const style = @import("./focus/style.zig"); 21 | pub const mach_compat = @import("./focus/mach_compat.zig"); 22 | 23 | const std = @import("std"); 24 | const u = util; 25 | const c = util.c; 26 | 27 | pub const Request = union(enum) { 28 | CreateEmptyWindow, 29 | CreateLauncherWindow, 30 | CreateEditorWindow: []const u8, 31 | }; 32 | 33 | // TODO this should just be std.net.Address, but it calculates the wrong address length for abstract domain sockets 34 | pub const Address = struct { 35 | address: std.net.Address, 36 | address_len: std.posix.socklen_t, 37 | }; 38 | 39 | pub const RequestAndClientAddress = struct { 40 | request: Request, 41 | client_address: Address, 42 | }; 43 | 44 | pub const ServerSocket = struct { 45 | address: Address, 46 | socket: std.posix.socket_t, 47 | state: State, 48 | 49 | const State = enum { 50 | Bound, 51 | Unbound, 52 | }; 53 | }; 54 | 55 | pub fn daemonize(log_filename: []const u8) enum { Parent, Child } { 56 | // https://stackoverflow.com/questions/17954432/creating-a-daemon-in-linux/17955149#17955149 57 | if (std.posix.fork()) |pid| { 58 | if (pid != 0) return .Parent; 59 | } else |err| { 60 | u.panic("Failed to fork: {}", .{err}); 61 | } 62 | { 63 | const err = c.setsid(); 64 | if (err < 0) u.panic("Failed to setsid: {}", .{err}); 65 | } 66 | // TODO 67 | //c.signal(c.SIGCHLD, std.posix.linux.SIG_IGN); 68 | //c.signal(c.SIGHUP, std.posix.linux.SIG_IGN); 69 | if (std.posix.fork()) |pid| { 70 | if (pid != 0) std.posix.exit(0); 71 | } else |err| { 72 | u.panic("Failed to fork: {}", .{err}); 73 | } 74 | std.posix.chdir(@ptrCast(config.home_path)) catch 75 | u.panic("Failed to chdir", .{}); 76 | 77 | // redirect stdout/err to log 78 | const log = std.fs.cwd().createFile(log_filename, .{ .read = false, .truncate = false }) catch |err| u.panic("Failed to open log file ({s}): {}", .{ log_filename, err }); 79 | log.seekFromEnd(0) catch |err| u.panic("Failed to seek to end of log file: {}", .{err}); 80 | _ = std.posix.dup2(log.handle, std.io.getStdOut().handle) catch |err| u.panic("Failed to redirect stdout to log: {}", .{err}); 81 | _ = std.posix.dup2(log.handle, std.io.getStdErr().handle) catch |err| u.panic("Failed to redirect stderr to log: {}", .{err}); 82 | std.debug.print("\n\nFocus daemon started at {}\n", .{std.time.timestamp()}); 83 | 84 | return .Child; 85 | } 86 | 87 | pub fn createServerSocket(socket_path: []const u8) ServerSocket { 88 | var address = std.net.Address.initUnix(socket_path) catch |err| u.panic("Failed to init unix socket address: {}", .{err}); 89 | // have to get len before setting the null byte or returns wrong address 90 | const address_len = address.getOsSockLen(); 91 | address.un.path[0] = 0; 92 | const socket = std.posix.socket(std.posix.AF.UNIX, std.posix.SOCK.DGRAM | std.posix.SOCK.CLOEXEC, 0) catch |err| u.panic("Failed to create unix socket: {}", .{err}); 93 | const state: ServerSocket.State = if (std.posix.bind(socket, &address.any, address_len)) 94 | .Bound 95 | else |err| switch (err) { 96 | error.AddressInUse => .Unbound, 97 | else => u.panic("Failed to connect to unix socket: {}", .{err}), 98 | }; 99 | return ServerSocket{ 100 | .address = .{ 101 | .address = address, 102 | .address_len = address_len, 103 | }, 104 | .socket = socket, 105 | .state = state, 106 | }; 107 | } 108 | 109 | pub const ClientSocket = struct { 110 | socket: std.posix.socket_t, 111 | }; 112 | 113 | pub fn createClientSocket() std.posix.socket_t { 114 | // with zero-length name, will get an autogenerated name 115 | var address = std.net.Address.initUnix("") catch |err| u.panic("Failed to init unix socket address: {}", .{err}); 116 | // TODO address.getOsSockLen() returns the wrong length for autogenerated names 117 | const address_len = @sizeOf(c_short); 118 | address.un.path[0] = 0; 119 | const socket = std.posix.socket(std.posix.AF.UNIX, std.posix.SOCK.DGRAM, 0) catch |err| u.panic("Failed to create unix socket: {}", .{err}); 120 | std.posix.bind(socket, &address.any, address_len) catch |err| u.panic("Failed to connect to unix socket: {}", .{err}); 121 | return socket; 122 | } 123 | 124 | pub fn sendRequest(client_socket: std.posix.socket_t, server_socket: ServerSocket, request: Request) void { 125 | const message = switch (request) { 126 | .CreateEmptyWindow => "CreateEmptyWindow", 127 | .CreateLauncherWindow => "CreateLauncherWindow", 128 | .CreateEditorWindow => |filename| filename, 129 | }; 130 | const len = std.posix.sendto(client_socket, message, 0, &server_socket.address.address.any, server_socket.address.address_len) catch |err| u.panic("Failed to send request: {}", .{err}); 131 | u.assert(len == message.len); 132 | } 133 | 134 | pub fn receiveRequest(buffer: []u8, server_socket: ServerSocket) ?RequestAndClientAddress { 135 | var client_address: std.posix.sockaddr = undefined; 136 | // TODO have no idea if this is the correct value 137 | var client_address_len: std.posix.socklen_t = @sizeOf(std.posix.sockaddr); 138 | const len = std.posix.recvfrom(server_socket.socket, buffer, std.posix.MSG.DONTWAIT, &client_address, &client_address_len) catch |err| { 139 | switch (err) { 140 | error.WouldBlock => return null, 141 | else => u.panic("Failed to recv request: {}", .{err}), 142 | } 143 | }; 144 | const message = buffer[0..len]; 145 | // TODO Relying on anon->Request coercion here breaks - in ReleaseSafe we get .{.CreateEditorWindow = "CreateEmptyWindow"} 146 | const request = if (std.mem.eql(u8, message, "CreateEmptyWindow")) 147 | Request{ .CreateEmptyWindow = {} } 148 | else if (std.mem.eql(u8, message, "CreateLauncherWindow")) 149 | Request{ .CreateLauncherWindow = {} } 150 | else 151 | Request{ .CreateEditorWindow = message }; 152 | return RequestAndClientAddress{ 153 | .request = request, 154 | .client_address = .{ 155 | .address = .{ .any = client_address }, 156 | .address_len = client_address_len, 157 | }, 158 | }; 159 | } 160 | 161 | pub fn sendReply(server_socket: ServerSocket, client_address: Address, exit_code: u8) void { 162 | _ = std.posix.sendto(server_socket.socket, &[1]u8{exit_code}, 0, &client_address.address.any, client_address.address_len) catch |err| u.warn("Failed to send reply: {}\n", .{err}); 163 | } 164 | 165 | // returns exit code 166 | pub fn waitReply(client_socket: std.posix.socket_t) u8 { 167 | var buffer: [1]u8 = undefined; 168 | const len = std.posix.recv(client_socket, &buffer, 0) catch |err| { 169 | u.panic("Failed to recv reply: {}", .{err}); 170 | }; 171 | u.assert(len == 1); 172 | return buffer[0]; 173 | } 174 | 175 | const ns_per_frame = @divTrunc(1_000_000_000, 60); 176 | 177 | pub fn run(allocator: u.Allocator, server_socket: ServerSocket) void { 178 | var app = App.init(allocator, server_socket); 179 | var timer = std.time.Timer.start() catch u.panic("Couldn't start timer", .{}); 180 | while (true) { 181 | _ = timer.lap(); 182 | app.frame(); 183 | const used_ns = timer.read(); 184 | if (used_ns < ns_per_frame) 185 | // TODO can we correct for drift from sleep imprecision? 186 | std.time.sleep(ns_per_frame - used_ns) 187 | else 188 | u.warn("Frame took {} ns\n", .{used_ns}); 189 | } 190 | } 191 | 192 | pub const App = struct { 193 | allocator: u.Allocator, 194 | server_socket: ServerSocket, 195 | frame_arena: u.ArenaAllocator, 196 | frame_allocator: u.Allocator, 197 | atlas: *Atlas, 198 | // contains only buffers that were created from files 199 | // other buffers are just floating around but must be deinited by their owning view 200 | buffers: u.DeepHashMap([]const u8, *Buffer), 201 | windows: u.ArrayList(*Window), 202 | frame_time_ms: i64, 203 | // used for both buffer_searcher and project_searcher 204 | last_search_filter: []const u8, 205 | last_project_search_selected: usize, 206 | last_file_filter: []const u8, 207 | last_project_file_opener_selected: usize, 208 | last_buffer_opener_selected: usize, 209 | last_error_lister_selected: usize, 210 | 211 | fn glfw_error_callback(error_code: c_int, description: [*c]const u8) callconv(.C) void { 212 | std.debug.print("{}: {s}\n", .{ error_code, description }); 213 | } 214 | 215 | pub fn init(allocator: u.Allocator, server_socket: ServerSocket) *App { 216 | _ = c.glfwSetErrorCallback(glfw_error_callback); 217 | if (c.glfwInit() != c.GLFW_TRUE) 218 | u.panic("Error starting glfw", .{}); 219 | 220 | const atlas = allocator.create(Atlas) catch u.oom(); 221 | atlas.* = Atlas.init(allocator, 16); 222 | var self = allocator.create(App) catch u.oom(); 223 | self.* = App{ 224 | .allocator = allocator, 225 | .server_socket = server_socket, 226 | .frame_arena = u.ArenaAllocator.init(allocator), 227 | .frame_allocator = undefined, 228 | .atlas = atlas, 229 | .buffers = u.DeepHashMap([]const u8, *Buffer).init(allocator), 230 | .windows = u.ArrayList(*Window).init(allocator), 231 | .frame_time_ms = 0, 232 | .last_search_filter = "", 233 | .last_project_search_selected = 0, 234 | .last_file_filter = "", 235 | .last_project_file_opener_selected = 0, 236 | .last_buffer_opener_selected = 0, 237 | .last_error_lister_selected = 0, 238 | }; 239 | self.frame_allocator = self.frame_arena.allocator(); 240 | 241 | return self; 242 | } 243 | 244 | pub fn deinit(self: *App) void { 245 | self.allocator.free(self.last_file_filter); 246 | self.allocator.free(self.last_search_filter); 247 | 248 | for (self.windows.items) |window| { 249 | window.deinit(); 250 | self.allocator.destroy(window); 251 | } 252 | self.windows.deinit(); 253 | 254 | var buffer_iter = self.buffers.iterator(); 255 | while (buffer_iter.next()) |entry| { 256 | self.allocator.free(entry.key_ptr.*); 257 | entry.value_ptr.deinit(); 258 | } 259 | self.buffers.deinit(); 260 | 261 | self.atlas.deinit(); 262 | self.allocator.destroy(self.atlas); 263 | 264 | self.frame_arena.deinit(); 265 | 266 | self.allocator.destroy(self); 267 | 268 | if (u.builtin.mode == .Debug) { 269 | _ = @import("root").gpa.detectLeaks(); 270 | } 271 | 272 | c.glfwTerminate(); 273 | } 274 | 275 | pub fn quit(self: *App) noreturn { 276 | self.deinit(); 277 | std.posix.exit(0); 278 | } 279 | 280 | pub fn getBufferFromAbsoluteFilename(self: *App, absolute_filename: []const u8) *Buffer { 281 | if (self.buffers.get(absolute_filename)) |buffer| { 282 | return buffer; 283 | } else { 284 | const buffer = Buffer.initFromAbsoluteFilename(self, .{}, absolute_filename); 285 | // buffer might be out of date if file was modified elsewhere 286 | buffer.refresh(); 287 | self.buffers.put(self.dupe(absolute_filename), buffer) catch u.oom(); 288 | return buffer; 289 | } 290 | } 291 | 292 | pub fn registerWindow(self: *App, window: Window) *Window { 293 | const window_ptr = self.allocator.create(Window) catch u.oom(); 294 | window_ptr.* = window; 295 | self.windows.append(window_ptr) catch u.oom(); 296 | return window_ptr; 297 | } 298 | 299 | pub fn deregisterWindow(self: *App, window: *Window) void { 300 | const i = std.mem.indexOfScalar(*Window, self.windows.items, window).?; 301 | _ = self.windows.swapRemove(i); 302 | self.allocator.destroy(window); 303 | } 304 | 305 | pub fn handleRequest(self: *App, request: Request, client_address: Address) void { 306 | var new_window: *Window = undefined; 307 | switch (request) { 308 | .CreateEmptyWindow => { 309 | new_window = self.registerWindow(Window.init(self, .NotFloating)); 310 | new_window.client_address_o = client_address; 311 | }, 312 | .CreateLauncherWindow => { 313 | new_window = self.registerWindow(Window.init(self, .Floating)); 314 | new_window.client_address_o = client_address; 315 | const launcher = Launcher.init(self); 316 | new_window.pushView(launcher); 317 | }, 318 | .CreateEditorWindow => |filename| { 319 | new_window = self.registerWindow(Window.init(self, .NotFloating)); 320 | new_window.client_address_o = client_address; 321 | const new_buffer = self.getBufferFromAbsoluteFilename(filename); 322 | const new_editor = Editor.init(self, new_buffer, .{}); 323 | new_window.pushView(new_editor); 324 | }, 325 | } 326 | } 327 | 328 | pub fn frame(self: *App) void { 329 | self.frame_time_ms = std.time.milliTimestamp(); 330 | 331 | // reset arena 332 | self.frame_arena.deinit(); 333 | self.frame_arena = u.ArenaAllocator.init(self.allocator); 334 | 335 | // check for requests 336 | const buffer = self.frame_allocator.alloc(u8, 256 * 1024) catch u.oom(); 337 | while (receiveRequest(buffer, self.server_socket)) |request_and_client_address| { 338 | self.handleRequest(request_and_client_address.request, request_and_client_address.client_address); 339 | } 340 | 341 | // refresh visible buffers 342 | for (self.windows.items) |window| { 343 | if (window.getTopViewIfEditor()) |editor| 344 | editor.buffer.refresh(); 345 | } 346 | 347 | // poll events 348 | // TODO use waitEventsTimeout to handle fps 349 | c.glfwPollEvents(); 350 | 351 | // run window frames 352 | // copy window list because it might change during frame 353 | const current_windows = self.frame_allocator.dupe(*Window, self.windows.items) catch u.oom(); 354 | for (current_windows) |window| window.frame(); 355 | } 356 | 357 | pub fn dupe(self: *App, slice: anytype) @TypeOf(slice) { 358 | return self.allocator.dupe(@typeInfo(@TypeOf(slice)).pointer.child, slice) catch u.oom(); 359 | } 360 | 361 | pub fn changeFontSize(self: *App, increment: isize) void { 362 | self.atlas.deinit(); 363 | const new_char_size_pixels = @as(isize, @intCast(self.atlas.char_size_pixels)) + increment; 364 | if (new_char_size_pixels >= 0) { 365 | self.atlas.* = Atlas.init(self.allocator, @intCast(new_char_size_pixels)); 366 | for (self.windows.items) |window| { 367 | window.loadAtlasTexture(self.atlas); 368 | } 369 | } 370 | } 371 | 372 | pub fn getCompletions(self: *App, language: std.meta.Tag(Language), prefix: []const u8) [][]const u8 { 373 | var results = u.ArrayList([]const u8).init(self.frame_allocator); 374 | 375 | var buffer_iter = self.buffers.iterator(); 376 | while (buffer_iter.next()) |entry| { 377 | if (std.meta.activeTag(entry.value_ptr.*.language) == language) 378 | entry.value_ptr.*.getCompletionsInto(prefix, &results); 379 | } 380 | 381 | std.mem.sort([]const u8, results.items, {}, struct { 382 | fn lessThan(_: void, a: []const u8, b: []const u8) bool { 383 | return std.mem.lessThan(u8, a, b); 384 | } 385 | }.lessThan); 386 | 387 | var unique_results = u.ArrayList([]const u8).init(self.frame_allocator); 388 | for (results.items, 0..) |result, i| { 389 | if (i == 0 or !std.mem.eql(u8, result, results.items[i - 1])) 390 | unique_results.append(result) catch u.oom(); 391 | } 392 | 393 | return unique_results.toOwnedSlice() catch u.oom(); 394 | } 395 | 396 | pub fn handleAfterSave(self: *App) void { 397 | for (self.windows.items) |window| window.handleAfterSave(); 398 | } 399 | }; 400 | -------------------------------------------------------------------------------- /fonts/Fira_Code_v5.2/README.txt: -------------------------------------------------------------------------------- 1 | Installing 2 | ========== 3 | 4 | Windows 5 | ------- 6 | 7 | In the ttf folder, double-click each font file, click “Install font”; to install all at once, select all files, right-click, and choose “Install” 8 | 9 | OR 10 | 11 | Use https://chocolatey.org: 12 | 13 | choco install firacode-ttf 14 | 15 | 16 | macOS 17 | ----- 18 | 19 | In the downloaded TTF folder: 20 | 21 | 1. Select all font files 22 | 2. Right click and select `Open` (alternatively `Open With Font Book`) 23 | 3. Select "Install Font" 24 | 25 | OR 26 | 27 | Use http://brew.sh: 28 | 29 | `brew tap homebrew/cask-fonts` 30 | `brew cask install font-fira-code` 31 | 32 | 33 | Ubuntu Zesty (17.04), Debian Stretch (9) or newer 34 | ------------------------------------------------- 35 | 36 | 1. Make sure that the `universe` (for Ubuntu) or `contrib` (for Debian) repository is enabled (see https://askubuntu.com/questions/148638/how-do-i-enable-the-universe-repository or https://wiki.debian.org/SourcesList#Component) 37 | 2. Install `fonts-firacode` package either by executing `sudo apt install fonts-firacode` in the terminal or via GUI tool (like “Software Center”) 38 | 39 | 40 | Arch Linux 41 | ---------- 42 | 43 | Fira Code package is available in the official repository: https://www.archlinux.org/packages/community/any/otf-fira-code/. 44 | 45 | Variant of Fira Code package is available in the AUR: https://aur.archlinux.org/packages/otf-fira-code-git/. 46 | 47 | 48 | Gentoo 49 | ------ 50 | 51 | emerge -av media-fonts/fira-code 52 | 53 | 54 | Fedora 55 | ------ 56 | 57 | A Fedora copr repository is available: https://copr.fedorainfracloud.org/coprs/evana/fira-code-fonts/. Package sources https://gitlab.com/evana11/fira-code-fonts-fedora. 58 | 59 | To install, perform the following commands: 60 | 61 | dnf copr enable evana/fira-code-fonts 62 | dnf install fira-code-fonts 63 | 64 | 65 | Solus 66 | ----- 67 | 68 | Fira Code package is available in the official repository: `font-firacode-ttf` and `font-firacode-otf`. 69 | They can be installed by running: 70 | 71 | sudo eopkg install font-firacode-ttf font-firacode-otf 72 | 73 | 74 | Void linux 75 | ---------- 76 | 77 | xbps-install font-firacode 78 | 79 | 80 | Linux Manual Installation 81 | ------------------------- 82 | 83 | With most desktop-oriented distributions, double-clicking each font file in the ttf folder and selecting “Install font” should be enough. If it isn’t, create and run `download_and_install.sh` script: 84 | 85 | #!/usr/bin/env bash 86 | 87 | fonts_dir="${HOME}/.local/share/fonts" 88 | if [ ! -d "${fonts_dir}" ]; then 89 | echo "mkdir -p $fonts_dir" 90 | mkdir -p "${fonts_dir}" 91 | else 92 | echo "Found fonts dir $fonts_dir" 93 | fi 94 | 95 | for type in Bold Light Medium Regular Retina; do 96 | file_path="${HOME}/.local/share/fonts/FiraCode-${type}.ttf" 97 | file_url="https://github.com/tonsky/FiraCode/blob/master/distr/ttf/FiraCode-${type}.ttf?raw=true" 98 | if [ ! -e "${file_path}" ]; then 99 | echo "wget -O $file_path $file_url" 100 | wget -O "${file_path}" "${file_url}" 101 | else 102 | echo "Found existing file $file_path" 103 | fi; 104 | done 105 | 106 | echo "fc-cache -f" 107 | fc-cache -f 108 | 109 | More details: https://github.com/tonsky/FiraCode/issues/4 110 | 111 | 112 | FreeBSD 113 | ------- 114 | 115 | Using pkg(8): 116 | 117 | pkg install firacode 118 | 119 | OR 120 | 121 | Using ports: 122 | 123 | cd /usr/ports/x11-fonts/firacode && make install clean 124 | 125 | 126 | Enabling ligatures 127 | ================== 128 | 129 | Atom 130 | ---- 131 | 132 | To change your font to Fira Code, open Atom's preferences (`cmd + ,` on a Mac, `ctrl + ,` on PC), make sure the "Settings" tab is selected, or the "Editor" in Atom 1.10+, and scroll down to "Editor Settings". In the "Font Family" field, enter `Fira Code`. 133 | 134 | If you wish to specify a font weight, for example, Light, use `Fira Code Light` as a font name (Windows) or `FiraCode-Light` (macOS). 135 | 136 | Ligatures are enabled by default in Atom 1.9 and above. 137 | 138 | 139 | VS Code 140 | ------- 141 | 142 | To open the settings editor, first from the File menu choose Preferences, Settings or use keyboard shortcut `Ctrl + ,` (Windows) or `Cmd + ,` (macOS). 143 | 144 | To enable FiraCode in the settings editor, under "Commonly Used", expand the "Text Editor" settings and then click on "Font". In the "Font Family" input box type `Fira Code`, replacing any content. Tick the check box "Enables/Disables font ligatures" under "Font Ligatures" to enable the special ligatures. 145 | 146 | If you wish to specify a font weight, for example, Light, use `Fira Code Light` as a font name (Windows) or `FiraCode-Light` (macOS). 147 | 148 | 149 | IntelliJ products 150 | ----------------- 151 | 152 | 1. Enable in Settings → Editor → Font → Enable Font Ligatures 153 | 2. Select `Fira Code` as "Primary font" under Settings → Editor → Font 154 | 155 | Additionally, if a Color Scheme is selected: 156 | 157 | 3. Enable in Settings → Editor → Color Scheme → Color Scheme Font → Enable Font Ligatures 158 | 4. Select Fira Code as "Primary font" under Settings → Editor → Color Scheme → Color Scheme Font 159 | 160 | 161 | BBEdit, TextWrangler 162 | -------------------- 163 | 164 | Run in your terminal: 165 | 166 | defaults write com.barebones.bbedit "EnableFontLigatures_Fira Code" -bool YES 167 | 168 | Source: https://www.barebones.com/support/bbedit/ExpertPreferences.html 169 | 170 | 171 | Brackets 172 | -------- 173 | 174 | 1. From the `View` menu choose `Themes....` 175 | 2. Paste `'Fira Code'`, at the begining of `Font Family` 176 | 177 | 178 | Emacs 179 | ----- 180 | 181 | There are a few options when it comes down to using ligatures in 182 | Emacs. They are listed in order of preferred to less-preferred. Pick one! 183 | 184 | 1. Using composition mode in Emacs Mac port 185 | 186 | If you're using the latest Mac port of Emacs (https://bitbucket.org/mituharu/emacs-mac by Mitsuharu Yamamoto) for macOS, you can use: 187 | 188 | (mac-auto-operator-composition-mode) 189 | 190 | This is generally the easiest solution, but can only be used on macOS. 191 | 192 | 2. Using prettify-symbols 193 | 194 | These instructions are pieced together by https://github.com/Triavanicus, taking some pieces from https://github.com/minad/hasklig-mode. 195 | 196 | This method requires you to install the Fira Code Symbol font, made by https://github.com/siegebell: 197 | https://github.com/tonsky/FiraCode/issues/211#issuecomment-239058632 198 | 199 | (defun fira-code-mode--make-alist (list) 200 | "Generate prettify-symbols alist from LIST." 201 | (let ((idx -1)) 202 | (mapcar 203 | (lambda (s) 204 | (setq idx (1+ idx)) 205 | (let* ((code (+ #Xe100 idx)) 206 | (width (string-width s)) 207 | (prefix ()) 208 | (suffix '(?\s (Br . Br))) 209 | (n 1)) 210 | (while (< n width) 211 | (setq prefix (append prefix '(?\s (Br . Bl)))) 212 | (setq n (1+ n))) 213 | (cons s (append prefix suffix (list (decode-char 'ucs code)))))) 214 | list))) 215 | 216 | (defconst fira-code-mode--ligatures 217 | '("www" "**" "***" "**/" "*>" "*/" "\\\\" "\\\\\\" 218 | "{-" "[]" "::" ":::" ":=" "!!" "!=" "!==" "-}" 219 | "--" "---" "-->" "->" "->>" "-<" "-<<" "-~" 220 | "#{" "#[" "##" "###" "####" "#(" "#?" "#_" "#_(" 221 | ".-" ".=" ".." "..<" "..." "?=" "??" ";;" "/*" 222 | "/**" "/=" "/==" "/>" "//" "///" "&&" "||" "||=" 223 | "|=" "|>" "^=" "$>" "++" "+++" "+>" "=:=" "==" 224 | "===" "==>" "=>" "=>>" "<=" "=<<" "=/=" ">-" ">=" 225 | ">=>" ">>" ">>-" ">>=" ">>>" "<*" "<*>" "<|" "<|>" 226 | "<$" "<$>" "\\)" #Xe113) 352 | ("[^-]\\(->\\)" #Xe114) 353 | ("\\(->>\\)" #Xe115) 354 | ("\\(-<\\)" #Xe116) 355 | ("\\(-<<\\)" #Xe117) 356 | ("\\(-~\\)" #Xe118) 357 | ("\\(#{\\)" #Xe119) 358 | ("\\(#\\[\\)" #Xe11a) 359 | ("\\(##\\)" #Xe11b) 360 | ("\\(###\\)" #Xe11c) 361 | ("\\(####\\)" #Xe11d) 362 | ("\\(#(\\)" #Xe11e) 363 | ("\\(#\\?\\)" #Xe11f) 364 | ("\\(#_\\)" #Xe120) 365 | ("\\(#_(\\)" #Xe121) 366 | ("\\(\\.-\\)" #Xe122) 367 | ("\\(\\.=\\)" #Xe123) 368 | ("\\(\\.\\.\\)" #Xe124) 369 | ("\\(\\.\\.<\\)" #Xe125) 370 | ("\\(\\.\\.\\.\\)" #Xe126) 371 | ("\\(\\?=\\)" #Xe127) 372 | ("\\(\\?\\?\\)" #Xe128) 373 | ("\\(;;\\)" #Xe129) 374 | ("\\(/\\*\\)" #Xe12a) 375 | ("\\(/\\*\\*\\)" #Xe12b) 376 | ("\\(/=\\)" #Xe12c) 377 | ("\\(/==\\)" #Xe12d) 378 | ("\\(/>\\)" #Xe12e) 379 | ("\\(//\\)" #Xe12f) 380 | ("\\(///\\)" #Xe130) 381 | ("\\(&&\\)" #Xe131) 382 | ("\\(||\\)" #Xe132) 383 | ("\\(||=\\)" #Xe133) 384 | ("[^|]\\(|=\\)" #Xe134) 385 | ("\\(|>\\)" #Xe135) 386 | ("\\(\\^=\\)" #Xe136) 387 | ("\\(\\$>\\)" #Xe137) 388 | ("\\(\\+\\+\\)" #Xe138) 389 | ("\\(\\+\\+\\+\\)" #Xe139) 390 | ("\\(\\+>\\)" #Xe13a) 391 | ("\\(=:=\\)" #Xe13b) 392 | ("[^!/]\\(==\\)[^>]" #Xe13c) 393 | ("\\(===\\)" #Xe13d) 394 | ("\\(==>\\)" #Xe13e) 395 | ("[^=]\\(=>\\)" #Xe13f) 396 | ("\\(=>>\\)" #Xe140) 397 | ("\\(<=\\)" #Xe141) 398 | ("\\(=<<\\)" #Xe142) 399 | ("\\(=/=\\)" #Xe143) 400 | ("\\(>-\\)" #Xe144) 401 | ("\\(>=\\)" #Xe145) 402 | ("\\(>=>\\)" #Xe146) 403 | ("[^-=]\\(>>\\)" #Xe147) 404 | ("\\(>>-\\)" #Xe148) 405 | ("\\(>>=\\)" #Xe149) 406 | ("\\(>>>\\)" #Xe14a) 407 | ("\\(<\\*\\)" #Xe14b) 408 | ("\\(<\\*>\\)" #Xe14c) 409 | ("\\(<|\\)" #Xe14d) 410 | ("\\(<|>\\)" #Xe14e) 411 | ("\\(<\\$\\)" #Xe14f) 412 | ("\\(<\\$>\\)" #Xe150) 413 | ("\\( Settings 542 | 543 | Add before "ignored_packages": 544 | 545 | "font_face": "Fira Code", 546 | "font_options": ["subpixel_antialias"], 547 | 548 | If you want enable antialias, add in font_options: "gray_antialias" 549 | 550 | 551 | Visual Studio 552 | ------------- 553 | 554 | 1. Launch Visual Studio (2015 or later). 555 | 2. Launch the Options dialog by opening the "Tools" menu and selecting "Options". 556 | 3. In the Options dialog, under the "Environment" category, you'll find "Fonts and Colors". Click on that. You'll see a combo-box on the right hand side of the dialog labelled "Font". Select "Fira Code" from that combo-box. 557 | 4. Click "OK" to dismiss. 558 | 5. Restart Visual Studio. 559 | 560 | Now, most FiraCode ligatures will work. A notable exception is the hyphen-based ligatures (e.g. the C++ dereference '->'). See https://github.com/tonsky/FiraCode/issues/422 for details. 561 | 562 | 563 | Troubleshooting 564 | =============== 565 | 566 | See https://github.com/tonsky/FiraCode/wiki/Troubleshooting 567 | -------------------------------------------------------------------------------- /lib/focus/window.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const focus = @import("../focus.zig"); 3 | const u = focus.util; 4 | const c = focus.util.c; 5 | const Atlas = focus.Atlas; 6 | const App = focus.App; 7 | const Editor = focus.Editor; 8 | const FileOpener = focus.FileOpener; 9 | const ProjectFileOpener = focus.ProjectFileOpener; 10 | const BufferOpener = focus.BufferOpener; 11 | const BufferSearcher = focus.BufferSearcher; 12 | const ProjectSearcher = focus.ProjectSearcher; 13 | const Launcher = focus.Launcher; 14 | const Maker = focus.Maker; 15 | const ErrorLister = focus.ErrorLister; 16 | const style = focus.style; 17 | const mach_compat = focus.mach_compat; 18 | 19 | pub const View = union(enum) { 20 | Editor: *Editor, 21 | FileOpener: *FileOpener, 22 | ProjectFileOpener: *ProjectFileOpener, 23 | BufferOpener: *BufferOpener, 24 | BufferSearcher: *BufferSearcher, 25 | ProjectSearcher: *ProjectSearcher, 26 | Launcher: *Launcher, 27 | Maker: *Maker, 28 | ErrorLister: *ErrorLister, 29 | }; 30 | 31 | pub const Window = struct { 32 | app: *App, 33 | // views are allowed to have pointers to previous views on the stack 34 | views: u.ArrayList(View), 35 | popped_views: u.ArrayList(View), 36 | close_after_frame: bool, 37 | 38 | // client socket who opened this window, need to tell them when we close 39 | client_address_o: ?focus.Address, 40 | 41 | events: *u.ArrayList(mach_compat.Event), 42 | glfw_window: *c.GLFWwindow, 43 | 44 | texture_buffer: u.ArrayList(u.Quad(u.Vec2f)), 45 | vertex_buffer: u.ArrayList(u.Quad(u.Vec2f)), 46 | color_buffer: u.ArrayList(u.Quad(u.Color)), 47 | index_buffer: u.ArrayList(u32), 48 | 49 | pub fn init( 50 | app: *App, 51 | floating: enum { Floating, NotFloating }, 52 | ) Window { 53 | // pretty arbitrary 54 | const init_width: usize = 1920; 55 | const init_height: usize = 1080; 56 | 57 | const events = app.allocator.create(u.ArrayList(mach_compat.Event)) catch u.oom(); 58 | events.* = u.ArrayList(mach_compat.Event).init(app.allocator); 59 | 60 | // init window 61 | const glfw_window_maybe = c.glfwCreateWindow(init_width, init_height, "focus", null, null); 62 | const glfw_window = if (glfw_window_maybe) |w| w else { 63 | u.panic("Error creating glfw window", .{}); 64 | }; 65 | c.glfwSetWindowAttrib(glfw_window, c.GLFW_DECORATED, c.GLFW_FALSE); 66 | c.glfwSetWindowAttrib(glfw_window, c.GLFW_RESIZABLE, c.GLFW_TRUE); 67 | c.glfwSetWindowAttrib(glfw_window, c.GLFW_FOCUS_ON_SHOW, c.GLFW_TRUE); 68 | c.glfwSetWindowAttrib(glfw_window, c.GLFW_FLOATING, switch (floating) { 69 | .Floating => c.GLFW_TRUE, 70 | .NotFloating => c.GLFW_FALSE, 71 | }); 72 | mach_compat.setCallbacks(glfw_window, events); 73 | 74 | // init gl 75 | c.glfwMakeContextCurrent(glfw_window); 76 | c.glEnable(c.GL_BLEND); 77 | c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA); 78 | c.glDisable(c.GL_CULL_FACE); 79 | c.glDisable(c.GL_DEPTH_TEST); 80 | c.glEnable(c.GL_TEXTURE_2D); 81 | c.glEnableClientState(c.GL_VERTEX_ARRAY); 82 | c.glEnableClientState(c.GL_TEXTURE_COORD_ARRAY); 83 | c.glEnableClientState(c.GL_COLOR_ARRAY); 84 | 85 | // disable vsync - too many problems with multiple windows on multiple desktops 86 | c.glfwSwapInterval(0); 87 | 88 | const self = Window{ 89 | .app = app, 90 | .views = u.ArrayList(View).init(app.allocator), 91 | .popped_views = u.ArrayList(View).init(app.allocator), 92 | .close_after_frame = false, 93 | 94 | .client_address_o = null, 95 | 96 | .events = events, 97 | .glfw_window = glfw_window, 98 | 99 | .texture_buffer = u.ArrayList(u.Quad(u.Vec2f)).init(app.allocator), 100 | .vertex_buffer = u.ArrayList(u.Quad(u.Vec2f)).init(app.allocator), 101 | .color_buffer = u.ArrayList(u.Quad(u.Color)).init(app.allocator), 102 | .index_buffer = u.ArrayList(u32).init(app.allocator), 103 | }; 104 | 105 | self.loadAtlasTexture(app.atlas); 106 | 107 | return self; 108 | } 109 | 110 | pub fn loadAtlasTexture(self: Window, atlas: *Atlas) void { 111 | c.glfwMakeContextCurrent(self.glfw_window); 112 | { 113 | var id: u32 = undefined; 114 | c.glGenTextures(1, &id); 115 | c.glBindTexture(c.GL_TEXTURE_2D, id); 116 | c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_ALPHA, atlas.texture_dims.x, atlas.texture_dims.y, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, atlas.texture.ptr); 117 | c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST); 118 | c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); 119 | u.assert(c.glGetError() == 0); 120 | } 121 | } 122 | 123 | pub fn deinit(self: *Window) void { 124 | self.index_buffer.deinit(); 125 | self.color_buffer.deinit(); 126 | self.vertex_buffer.deinit(); 127 | self.texture_buffer.deinit(); 128 | 129 | // TODO do I need to destroy the gl context? 130 | c.glfwDestroyWindow(self.glfw_window); 131 | 132 | self.events.deinit(); 133 | self.app.allocator.destroy(self.events); 134 | 135 | while (self.views.items.len > 0) { 136 | const view = self.views.pop().?; 137 | self.popped_views.append(view) catch u.oom(); 138 | } 139 | self.deinitPoppedViews(); 140 | self.popped_views.deinit(); 141 | self.views.deinit(); 142 | } 143 | 144 | pub fn getTopView(self: *Window) ?View { 145 | if (self.views.items.len > 0) 146 | return self.views.items[self.views.items.len - 1] 147 | else 148 | return null; 149 | } 150 | 151 | pub fn getTopViewIfEditor(self: *Window) ?*Editor { 152 | if (self.getTopView()) |view| { 153 | switch (view) { 154 | .Editor => |editor| return editor, 155 | else => return null, 156 | } 157 | } else return null; 158 | } 159 | 160 | fn getTopViewFilename(self: *Window) ?[]const u8 { 161 | if (self.getTopViewIfEditor()) |editor| 162 | return editor.buffer.getFilename() 163 | else 164 | return null; 165 | } 166 | 167 | pub fn frame(self: *Window) void { 168 | // figure out window size 169 | var window_width: c_int = 0; 170 | var window_height: c_int = 0; 171 | c.glfwGetWindowSize(self.glfw_window, &window_width, &window_height); 172 | const window_rect = u.Rect{ 173 | .x = 0, 174 | .y = 0, 175 | .w = @intCast(window_width), 176 | .h = @intCast(window_height), 177 | }; 178 | 179 | // handle events 180 | var view_events = u.ArrayList(mach_compat.Event).init(self.app.frame_allocator); 181 | { 182 | const events = self.events.toOwnedSlice() catch u.oom(); 183 | defer self.app.allocator.free(events); 184 | for (events) |event| { 185 | var handled = false; 186 | switch (event) { 187 | .key_press, .key_repeat => |key_event| { 188 | if (key_event.mods & c.GLFW_MOD_CONTROL != 0) { 189 | switch (key_event.key) { 190 | c.GLFW_KEY_Q => self.popView(), 191 | c.GLFW_KEY_O => { 192 | const init_path = if (self.getTopViewFilename()) |filename| 193 | std.mem.concat(self.app.frame_allocator, u8, &[_][]const u8{ std.fs.path.dirname(filename).?, "/" }) catch u.oom() 194 | else 195 | focus.config.home_path; 196 | const file_opener = FileOpener.init(self.app, init_path); 197 | self.pushView(file_opener); 198 | handled = true; 199 | }, 200 | c.GLFW_KEY_P => { 201 | const project_file_opener = ProjectFileOpener.init(self.app); 202 | self.pushView(project_file_opener); 203 | handled = true; 204 | }, 205 | c.GLFW_KEY_N => { 206 | if (self.getTopViewIfEditor()) |editor| { 207 | const new_window = self.app.registerWindow(Window.init(self.app, .NotFloating)); 208 | const new_editor = Editor.init(self.app, editor.buffer, .{}); 209 | new_editor.top_pixel = editor.top_pixel; 210 | new_window.pushView(new_editor); 211 | } 212 | handled = true; 213 | }, 214 | c.GLFW_KEY_MINUS => { 215 | self.app.changeFontSize(-1); 216 | handled = true; 217 | }, 218 | c.GLFW_KEY_EQUAL => { 219 | self.app.changeFontSize(1); 220 | handled = true; 221 | }, 222 | c.GLFW_KEY_M => { 223 | const maker = Maker.init(self.app); 224 | self.pushView(maker); 225 | handled = true; 226 | }, 227 | else => {}, 228 | } 229 | } 230 | if (key_event.mods & c.GLFW_MOD_ALT != 0) { 231 | switch (key_event.key) { 232 | c.GLFW_KEY_F => { 233 | const project_dir = if (self.getTopViewIfEditor()) |editor| 234 | editor.buffer.getProjectDir() 235 | else 236 | null; 237 | const project_searcher = ProjectSearcher.init( 238 | self.app, 239 | project_dir orelse focus.config.home_path, 240 | .FixedStrings, 241 | null, 242 | ); 243 | self.pushView(project_searcher); 244 | handled = true; 245 | }, 246 | c.GLFW_KEY_P => { 247 | const ignore_buffer = if (self.getTopViewIfEditor()) |editor| 248 | editor.buffer 249 | else 250 | null; 251 | const buffer_opener = BufferOpener.init(self.app, ignore_buffer); 252 | self.pushView(buffer_opener); 253 | handled = true; 254 | }, 255 | c.GLFW_KEY_M => { 256 | const error_lister = ErrorLister.init(self.app); 257 | self.pushView(error_lister); 258 | handled = true; 259 | }, 260 | else => {}, 261 | } 262 | } 263 | }, 264 | .focus_lost => { 265 | if (self.getTopViewIfEditor()) |editor| { 266 | editor.save(.Auto); 267 | editor.buffer.last_lost_focus_ms = self.app.frame_time_ms; 268 | } 269 | handled = true; 270 | }, 271 | .window_closed => { 272 | self.close_after_frame = true; 273 | handled = true; 274 | }, 275 | else => {}, 276 | } 277 | // delegate other events to view 278 | if (!handled) view_events.append(event) catch u.oom(); 279 | } 280 | } 281 | 282 | // run view frame 283 | if (self.getTopView()) |view| { 284 | switch (view) { 285 | .Editor => |editor| editor.frame(self, window_rect, view_events.items), 286 | .FileOpener => |file_opener| file_opener.frame(self, window_rect, view_events.items), 287 | .ProjectFileOpener => |project_file_opener| project_file_opener.frame(self, window_rect, view_events.items), 288 | .BufferOpener => |buffer_opener| buffer_opener.frame(self, window_rect, view_events.items), 289 | .BufferSearcher => |buffer_searcher| buffer_searcher.frame(self, window_rect, view_events.items), 290 | .ProjectSearcher => |project_searcher| project_searcher.frame(self, window_rect, view_events.items), 291 | .Launcher => |launcher| launcher.frame(self, window_rect, view_events.items), 292 | .Maker => |maker| maker.frame(self, window_rect, view_events.items), 293 | .ErrorLister => |error_lister| error_lister.frame(self, window_rect, view_events.items), 294 | } 295 | } else { 296 | const message = "focus"; 297 | const rect = u.Rect{ 298 | .x = window_rect.x + @max(0, @divTrunc(window_rect.w - (@as(u.Coord, @intCast(message.len)) * self.app.atlas.char_width), 2)), 299 | .y = window_rect.y + @max(0, @divTrunc(window_rect.h - self.app.atlas.char_height, 2)), 300 | .w = @min(window_rect.w, @as(u.Coord, @intCast(message.len)) * self.app.atlas.char_width), 301 | .h = @min(window_rect.h, self.app.atlas.char_height), 302 | }; 303 | self.queueText(rect, style.text_color, message); 304 | } 305 | 306 | // set window title 307 | var window_title: [*c]const u8 = ""; 308 | if (self.getTopViewFilename()) |filename| { 309 | window_title = self.app.frame_allocator.dupeZ(u8, filename) catch u.oom(); 310 | } 311 | c.glfwSetWindowTitle(self.glfw_window, window_title); 312 | 313 | // render 314 | c.glfwMakeContextCurrent(self.glfw_window); 315 | c.glClearColor(0, 0, 0, 1); 316 | c.glClear(c.GL_COLOR_BUFFER_BIT); 317 | c.glViewport( 318 | 0, 319 | 0, 320 | @intCast(window_width), 321 | @intCast(window_height), 322 | ); 323 | c.glMatrixMode(c.GL_PROJECTION); 324 | c.glPushMatrix(); 325 | c.glLoadIdentity(); 326 | c.glOrtho(0.0, @floatFromInt(window_width), @floatFromInt(window_height), 0.0, -1.0, 1.0); 327 | c.glMatrixMode(c.GL_MODELVIEW); 328 | c.glPushMatrix(); 329 | c.glLoadIdentity(); 330 | c.glTexCoordPointer(2, c.GL_FLOAT, 0, self.texture_buffer.items.ptr); 331 | c.glVertexPointer(2, c.GL_FLOAT, 0, self.vertex_buffer.items.ptr); 332 | c.glColorPointer(4, c.GL_UNSIGNED_BYTE, 0, self.color_buffer.items.ptr); 333 | c.glDrawElements(c.GL_TRIANGLES, @intCast(self.index_buffer.items.len), c.GL_UNSIGNED_INT, self.index_buffer.items.ptr); 334 | c.glMatrixMode(c.GL_MODELVIEW); 335 | c.glPopMatrix(); 336 | c.glMatrixMode(c.GL_PROJECTION); 337 | c.glPopMatrix(); 338 | c.glfwSwapBuffers(self.glfw_window); 339 | 340 | // reset 341 | self.texture_buffer.resize(0) catch u.oom(); 342 | self.vertex_buffer.resize(0) catch u.oom(); 343 | self.color_buffer.resize(0) catch u.oom(); 344 | self.index_buffer.resize(0) catch u.oom(); 345 | 346 | // clean up 347 | self.deinitPoppedViews(); 348 | if (self.close_after_frame) { 349 | if (self.getTopViewIfEditor()) |editor| editor.save(.Auto); 350 | if (self.client_address_o) |client_address| 351 | focus.sendReply(self.app.server_socket, client_address, 0); 352 | self.deinit(); 353 | self.app.deregisterWindow(self); 354 | } 355 | } 356 | 357 | pub fn queueQuad(self: *Window, dst: u.Rect, src: u.Rect, color: u.Color) void { 358 | const tx = @as(f32, @floatFromInt(src.x)) / @as(f32, @floatFromInt(self.app.atlas.texture_dims.x)); 359 | const ty = @as(f32, @floatFromInt(src.y)) / @as(f32, @floatFromInt(self.app.atlas.texture_dims.y)); 360 | const tw = @as(f32, @floatFromInt(src.w)) / @as(f32, @floatFromInt(self.app.atlas.texture_dims.x)); 361 | const th = @as(f32, @floatFromInt(src.h)) / @as(f32, @floatFromInt(self.app.atlas.texture_dims.y)); 362 | self.texture_buffer.append(.{ 363 | .tl = .{ .x = tx, .y = ty }, 364 | .tr = .{ .x = tx + tw, .y = ty }, 365 | .bl = .{ .x = tx, .y = ty + th }, 366 | .br = .{ .x = tx + tw, .y = ty + th }, 367 | }) catch u.oom(); 368 | 369 | const vx = @as(f32, @floatFromInt(dst.x)); 370 | const vy = @as(f32, @floatFromInt(dst.y)); 371 | const vw = @as(f32, @floatFromInt(dst.w)); 372 | const vh = @as(f32, @floatFromInt(dst.h)); 373 | const vertex_ix = @as(u32, @intCast(self.vertex_buffer.items.len * 4)); 374 | self.vertex_buffer.append(.{ 375 | .tl = .{ .x = vx, .y = vy }, 376 | .tr = .{ .x = vx + vw, .y = vy }, 377 | .bl = .{ .x = vx, .y = vy + vh }, 378 | .br = .{ .x = vx + vw, .y = vy + vh }, 379 | }) catch u.oom(); 380 | 381 | self.color_buffer.append(.{ 382 | .tl = color, 383 | .tr = color, 384 | .bl = color, 385 | .br = color, 386 | }) catch u.oom(); 387 | 388 | self.index_buffer.appendSlice(&.{ 389 | vertex_ix + 0, 390 | vertex_ix + 1, 391 | vertex_ix + 2, 392 | 393 | vertex_ix + 2, 394 | vertex_ix + 3, 395 | vertex_ix + 1, 396 | }) catch u.oom(); 397 | } 398 | 399 | pub fn handleAfterSave(self: *Window) void { 400 | if (self.getTopView()) |view| 401 | if (view == .Maker) 402 | view.Maker.handleAfterSave(); 403 | } 404 | 405 | // view api 406 | 407 | pub fn pushView(self: *Window, view_ptr: anytype) void { 408 | if (self.getTopViewIfEditor()) |editor| { 409 | editor.save(.Auto); 410 | editor.buffer.last_lost_focus_ms = self.app.frame_time_ms; 411 | } 412 | const tag_name = comptime tag_name: { 413 | // TODO this is gross 414 | const view_type_name = @typeName(@typeInfo(@TypeOf(view_ptr)).pointer.child); 415 | var iter = std.mem.splitScalar(u8, view_type_name, '.'); 416 | var last_part: ?[]const u8 = null; 417 | while (iter.next()) |part| last_part = part; 418 | break :tag_name last_part.?; 419 | }; 420 | const view = @unionInit(View, tag_name, view_ptr); 421 | self.views.append(view) catch u.oom(); 422 | } 423 | 424 | pub fn popView(self: *Window) void { 425 | if (self.getTopViewIfEditor()) |editor| { 426 | editor.save(.Auto); 427 | editor.buffer.last_lost_focus_ms = self.app.frame_time_ms; 428 | } 429 | if (self.views.items.len > 0) { 430 | const view = self.views.pop().?; 431 | // can't clean up view right away because we might still be inside it's frame function 432 | self.popped_views.append(view) catch u.oom(); 433 | } 434 | } 435 | 436 | fn deinitPoppedViews(self: *Window) void { 437 | while (self.popped_views.items.len > 0) { 438 | const view = self.popped_views.pop().?; 439 | switch (view) { 440 | .Editor => |editor| editor.save(.Auto), 441 | else => {}, 442 | } 443 | inline for (@typeInfo(@typeInfo(View).@"union".tag_type.?).@"enum".fields) |field| { 444 | if (@intFromEnum(std.meta.activeTag(view)) == field.value) { 445 | var view_ptr = @field(view, field.name); 446 | view_ptr.deinit(); 447 | } 448 | } 449 | } 450 | } 451 | 452 | // drawing api 453 | 454 | pub fn queueRect(self: *Window, rect: u.Rect, color: u.Color) void { 455 | self.queueQuad(rect, self.app.atlas.white_rect, color); 456 | } 457 | 458 | pub fn queueText(self: *Window, rect: u.Rect, color: u.Color, chars: []const u8) void { 459 | const max_x = rect.x + rect.w; 460 | const max_y = rect.y + rect.h; 461 | var dst: u.Rect = .{ .x = rect.x, .y = rect.y, .w = 0, .h = 0 }; 462 | for (chars) |char| { 463 | var src = if (char < self.app.atlas.char_to_rect.len) 464 | self.app.atlas.char_to_rect[char] 465 | else 466 | // tofu 467 | self.app.atlas.char_to_rect[0]; 468 | const max_w = @max(0, max_x - dst.x); 469 | const max_h = @max(0, max_y - dst.y); 470 | const ratio_w = @as(f64, @floatFromInt(@min(max_w, self.app.atlas.char_width))) / @as(f64, @floatFromInt(self.app.atlas.char_width)); 471 | const ratio_h = @as(f64, @floatFromInt(@min(max_h, self.app.atlas.char_height))) / @as(f64, @floatFromInt(self.app.atlas.char_height)); 472 | src.w = @intFromFloat(@floor(@as(f64, @floatFromInt(src.w)) * ratio_w)); 473 | src.h = @intFromFloat(@floor(@as(f64, @floatFromInt(src.h)) * ratio_h)); 474 | dst.w = src.w; 475 | dst.h = src.h; 476 | self.queueQuad(dst, src, color); 477 | dst.x += self.app.atlas.char_width; 478 | } 479 | } 480 | 481 | // util 482 | 483 | pub const SearcherLayout = struct { 484 | selector: u.Rect, 485 | input: u.Rect, 486 | }; 487 | 488 | pub fn layoutSearcher(self: *Window, rect: u.Rect) SearcherLayout { 489 | const border_thickness = @divTrunc(self.app.atlas.char_height, 8); 490 | var all_rect = rect; 491 | const input_rect = all_rect.splitTop(self.app.atlas.char_height, 0); 492 | const border_rect = all_rect.splitTop(border_thickness, 0); 493 | const selector_rect = all_rect; 494 | self.queueRect(border_rect, style.text_color); 495 | return .{ .selector = selector_rect, .input = input_rect }; 496 | } 497 | 498 | pub const SearcherWithPreviewLayout = struct { 499 | preview: u.Rect, 500 | selector: u.Rect, 501 | input: u.Rect, 502 | }; 503 | 504 | pub fn layoutSearcherWithPreview(self: *Window, rect: u.Rect) SearcherWithPreviewLayout { 505 | const border_thickness = @divTrunc(self.app.atlas.char_height, 8); 506 | var all_rect = rect; 507 | const h = @divTrunc(@max(0, rect.h - self.app.atlas.char_height - 2 * border_thickness), 2); 508 | const preview_rect = all_rect.splitTop(h, 0); 509 | const border_rect = all_rect.splitTop(border_thickness, 0); 510 | const searcher_layout = self.layoutSearcher(all_rect); 511 | self.queueRect(border_rect, style.text_color); 512 | return .{ .preview = preview_rect, .selector = searcher_layout.selector, .input = searcher_layout.input }; 513 | } 514 | 515 | pub const ListerLayout = struct { 516 | preview: u.Rect, 517 | report: u.Rect, 518 | }; 519 | 520 | pub fn layoutLister(self: *Window, rect: u.Rect) ListerLayout { 521 | const border_thickness = @divTrunc(self.app.atlas.char_height, 8); 522 | var all_rect = rect; 523 | const h = @divTrunc(@max(0, rect.h - 2 * border_thickness), 2); 524 | const preview_rect = all_rect.splitTop(h, 0); 525 | const border_rect = all_rect.splitTop(border_thickness, 0); 526 | const report_rect = all_rect; 527 | self.queueRect(border_rect, style.text_color); 528 | return .{ .preview = preview_rect, .report = report_rect }; 529 | } 530 | }; 531 | --------------------------------------------------------------------------------